تولید برنامههای اجرایی تک فایلی در زمان NET Core 3x. ارائه شد؛ اما به همراه این مسائل نیز بود:
- فایل اجرایی تک فایلی تولید شده در اصل یک فایل zip خود باز شونده بود که در یک مکان موقتی به صورت خودکار باز و اجرا میشد. این حالت با آنتیویروسها و یا سیستمهایی که قسمتهای اصلی آنها جهت کاربران عادی قفل شدهاند، مشکلاتی را ایجاد میکرد.
- حجم فایل نهایی تولید شده قابل توجه بود. برای نمونه یک برنامهی کنسول Hello world آن حدود 70 مگابایت میشد. البته باید درنظر داشت که یک چنین خروجی به همراه یک NET Core runtime. کامل نیز میشد.
از آن زمان تغییرات تدریجی مفیدی در این زمینه رخ دادهاند که خلاصهای از آنها را تا دات نت 6 در ادامه مرور میکنیم.
اصول تولید برنامههای اجرایی تک فایلی دات نت
فرض کنید برنامهی کنسول ما از این سه سطر تشکیل شدهاست:
برای تولید یک برنامهی اجرایی تک فایلی بر اساس آن، میتوان دستور زیر را در خط فرمان اجرا کرد:
در این حالت ذکر سیستم عامل هدف، اجباری است؛ از این جهت که خروجی نهایی تنها برای یک سیستم عامل تهیه میشود.
پس از اجرای دستور فوق، اگر به مکان C:\MyProject\bin\Release\net6.0\win-x64\publish مراجعه کنیم، به یک فایل exe حدود 62 مگابایتی خواهیم رسید که کمی کم حجمتر از نمونهی NET Core 3x. آن است! البته همانطور که عنوان شد این خروجی به همراه runtime متناظری نیز هست. اگر بخواهیم این runtime را از آن حذف کنیم میتوان به صورت زیر عمل کرد:
با استفاده از سوئیچ self-contained false دیگر خروجی نهایی به همراه runtime دات نت تشکیل نخواهد شد و حجم حاصل تنها 150 کیلوبایت خواهد بود. در این حالت استفاده کنندهی نهایی باید runtime را خودش به صورت مجزایی نصب کند.
یک نکته: میتوان سوئیچهای فوق را به فایل csproj نیز به صورت زیر اضافه کرد:
تک فایلهای اجرایی دات نت 6 دیگر فایلهای zip خود باز شونده نیستند
همانطور که عنوان شد، تک فایلهای اجرایی تولید شدهی در نگارشهای پیشین دات نت، چیزی بجز یک فایل zip خود بازشونده که همه چیز داخل آن قرار گرفته بودند، نبودند. این حالت دیگر در دات نت 6 صادق نیست و اینبار خروجی نهایی در حافظه بارگذاری میشود و نیاز به باز شدن آن در مکانهای temp برطرف شدهاست. تا زمان دات نت 5، این قابلیت فقط برای خروجیهای لینوکس تدارک دیده شده بود، اما با ارائهی دات نت 6، خروجیهای ویندوز و مک هم فایلهای اجرایی واقعی هستند.
فعالسازی IL Trimming
به صورت پیشفرض با اجرای دستورات تولید تک فایلهای اجرایی برنامههای دات نت، تمام وابستگیهای استفاده شده بدون هیچگونه بهینه سازی در کنار هم قرار میگیرند. با فعالسازی قابلیت IL Trimming میتوان وابستگیهایی را که برنامه از آنها استفاده نمیکند، از خروجی نهایی حذف کرد که در نتیجهی آن، شاهد کاهش حجم قابل ملاحظهی فایل تولیدی نهایی خواهیم بود. برای اینکار میتوان سوئیچ PublishTrimmed را فعالسازی کرد:
پس از آن برنامهی 60 مگابایتی تولیدی در ابتدای بحث، تبدیل به یک برنامهی اجرایی تک فایلی 11 مگابایتی میشود که کاهش حجم قابل توجهی است.
باید دقت داشت که این حجم نهایی، یک فایل اجرایی واقعی بدون نیاز به نصب هیچ نوع runtime ای است و کاملا متکی به خود است.
فعالسازی فشرده سازی
به همراه دات نت 6، امکان فشرده سازی خودکار این خروجی نهایی تک فایلی، جهت کاهش هرچه بیشتر حجم آن نیز میسر شدهاست. برای اینکار میتوان سوئیچ EnableCompressionInSingleFile را فعالسازی کرد:
خروجی آن یک فایل 30 مگابایتی بدون IL Trimming است که نسبت به خروجی 60 مگابایتی ابتدای بحث، باز هم کاهش قابل ملاحظهای داشتهاست.
- فایل اجرایی تک فایلی تولید شده در اصل یک فایل zip خود باز شونده بود که در یک مکان موقتی به صورت خودکار باز و اجرا میشد. این حالت با آنتیویروسها و یا سیستمهایی که قسمتهای اصلی آنها جهت کاربران عادی قفل شدهاند، مشکلاتی را ایجاد میکرد.
- حجم فایل نهایی تولید شده قابل توجه بود. برای نمونه یک برنامهی کنسول Hello world آن حدود 70 مگابایت میشد. البته باید درنظر داشت که یک چنین خروجی به همراه یک NET Core runtime. کامل نیز میشد.
از آن زمان تغییرات تدریجی مفیدی در این زمینه رخ دادهاند که خلاصهای از آنها را تا دات نت 6 در ادامه مرور میکنیم.
اصول تولید برنامههای اجرایی تک فایلی دات نت
فرض کنید برنامهی کنسول ما از این سه سطر تشکیل شدهاست:
using System; Console.WriteLine("Hello, World!"); Console.ReadLine();
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained true
پس از اجرای دستور فوق، اگر به مکان C:\MyProject\bin\Release\net6.0\win-x64\publish مراجعه کنیم، به یک فایل exe حدود 62 مگابایتی خواهیم رسید که کمی کم حجمتر از نمونهی NET Core 3x. آن است! البته همانطور که عنوان شد این خروجی به همراه runtime متناظری نیز هست. اگر بخواهیم این runtime را از آن حذف کنیم میتوان به صورت زیر عمل کرد:
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained false
یک نکته: میتوان سوئیچهای فوق را به فایل csproj نیز به صورت زیر اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <PublishSingleFile>true</PublishSingleFile> <SelfContained>true</SelfContained> <RuntimeIdentifier>win-x64</RuntimeIdentifier> <PublishReadyToRun>true</PublishReadyToRun> </PropertyGroup> </Project>
تک فایلهای اجرایی دات نت 6 دیگر فایلهای zip خود باز شونده نیستند
همانطور که عنوان شد، تک فایلهای اجرایی تولید شدهی در نگارشهای پیشین دات نت، چیزی بجز یک فایل zip خود بازشونده که همه چیز داخل آن قرار گرفته بودند، نبودند. این حالت دیگر در دات نت 6 صادق نیست و اینبار خروجی نهایی در حافظه بارگذاری میشود و نیاز به باز شدن آن در مکانهای temp برطرف شدهاست. تا زمان دات نت 5، این قابلیت فقط برای خروجیهای لینوکس تدارک دیده شده بود، اما با ارائهی دات نت 6، خروجیهای ویندوز و مک هم فایلهای اجرایی واقعی هستند.
فعالسازی IL Trimming
به صورت پیشفرض با اجرای دستورات تولید تک فایلهای اجرایی برنامههای دات نت، تمام وابستگیهای استفاده شده بدون هیچگونه بهینه سازی در کنار هم قرار میگیرند. با فعالسازی قابلیت IL Trimming میتوان وابستگیهایی را که برنامه از آنها استفاده نمیکند، از خروجی نهایی حذف کرد که در نتیجهی آن، شاهد کاهش حجم قابل ملاحظهی فایل تولیدی نهایی خواهیم بود. برای اینکار میتوان سوئیچ PublishTrimmed را فعالسازی کرد:
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained true -p:PublishTrimmed=true
باید دقت داشت که این حجم نهایی، یک فایل اجرایی واقعی بدون نیاز به نصب هیچ نوع runtime ای است و کاملا متکی به خود است.
فعالسازی فشرده سازی
به همراه دات نت 6، امکان فشرده سازی خودکار این خروجی نهایی تک فایلی، جهت کاهش هرچه بیشتر حجم آن نیز میسر شدهاست. برای اینکار میتوان سوئیچ EnableCompressionInSingleFile را فعالسازی کرد:
dotnet publish -p:PublishSingleFile=true -r win-x64 -c Release --self-contained true -p:EnableCompressionInSingleFile=true
پیشتر مطلب «Count یا Any » را در این سایت مطالعه کردهاید که در پایان آن این نتیجه گیری صورت گرفتهاست:
«از این پس حین استفاده از انواع و اقسام لیستها، آرایهها، IEnumerableها و امثال آنها، جهت بررسی خالی بودن یا نبودن آنها تنها از متد Any فراهم شده توسط LINQ استفاده نمائید.»
اکنون پس از سالها، قصد داریم صحت این مساله را با NET 5.0. بررسی کنیم که آیا هنوز هم متد Any، بهترین متد بررسی خالی بودن مجموعهها و آرایههای NET 5.0. است یا خیر؟
نحوهی بررسی کارآیی روشهای مختلف خالی بودن مجموعهها و آرایهها در C# 9.0
در ابتدا یک لیست، یک Enumerable و یک آرایه را به صورت زیر مقدار دهی میکنیم و هر سهی اینها میتوانند نال هم باشند:
اکنون که C# 9.0 در اختیار ما است به همراه pattern matching و همچنین Null Conditional Operator و غیره، میتوان روشهای زیر را برای بررسی خالی بودن این مجموعهها و آرایهها بکار گرفت:
1- استفاده از Null coalescing برای بررسی نال بودن مجموعه و سپس استفاده از متد Any برای بررسی خالی بودن آن:
2- استفاده از pattern matching برای بررسی نال بودن مجموعه و سپس استفاده از متد Any برای بررسی خالی بودن آن:
3- استفاده از روش سنتی مقایسهی مستقیم با null و سپس استفاده از متد Any برای بررسی خالی بودن آن:
4- استفاده از Null Conditional Operator برای بررسی نال بودن و سپس استفاده از متد Any برای بررسی خالی بودن آن:
5- استفاده از pattern matching برای بررسی مقدار خاصیت Count یک لیست یا آرایه. البته Enumerableها به همراه این خاصیت نیستند و یا باید آنها را به لیست و یا آرایه تبدیل کرد و یا میتوان متد ()Count آنها را فراخوانی نمود:
6- استفاده از Null Conditional Operator برای بررسی نال بودن و سپس استفاده از مقدار خاصیت Count لیست، برای بررسی خالی بودن آن:
7- استفاده از روش سنتی مقایسهی مستقیم با null و سپس استفاده از مقدار خاصیت Count لیست، برای بررسی خالی بودن آن:
کدهای کامل این بررسی به صورت زیر هستند: AnyCountBenchmark.zip
ابتدا ارجاعی به BenchmarkDotNet به برنامه اضافه شدهاست:
و سپس کدهای زیر، بررسی کارآیی روشهای مختلف تعیین خالی بودن مجموعهها را انجام میدهند:
به همراه سناریوهای مختلف زیر:
یکبار اجرای آن، نتیجهی زیر را به همراه داشت:
نتایج حاصل:
- بررسی خالی بودن آرایهها، بسیار سریعتر از بررسی خالی بودن لیستها و این مورد نیز سریعتر از Enumerableها است.
- اگر از آرایهها و یا لیستها استفاده میکنید، بررسی خاصیت Length و یا خاصیت Count آنها، بسیار سریعتر از بکارگیری متد Any بر روی آنها است.
- اگر از Enumerableها استفاده میکنید، استفاده از متد Any بر روی آنها، بسیار سریعتر از بکارگیری متد ()Count و یا تبدیل آنها به لیست و سپس بررسی خاصیت Count آنها است.
- بررسی نال بودن با pattern matching یا همان is null، نسبت به روشی سنتی استفادهی از null ==، سریعتر است. علت آنرا در مطلب «روش ترجیح داده شدهی مقایسه مقادیر اشیاء با null از زمان C# 7.0 به بعد» میتوانید مطالعه کنید.
بنابراین برای بررسی خالی بودن آرایهها و لیستها، بهتر است از خاصیت Length و یا Count آنها استفاده کرد و برای Enumerableها از متد ()Any.
«از این پس حین استفاده از انواع و اقسام لیستها، آرایهها، IEnumerableها و امثال آنها، جهت بررسی خالی بودن یا نبودن آنها تنها از متد Any فراهم شده توسط LINQ استفاده نمائید.»
اکنون پس از سالها، قصد داریم صحت این مساله را با NET 5.0. بررسی کنیم که آیا هنوز هم متد Any، بهترین متد بررسی خالی بودن مجموعهها و آرایههای NET 5.0. است یا خیر؟
نحوهی بررسی کارآیی روشهای مختلف خالی بودن مجموعهها و آرایهها در C# 9.0
در ابتدا یک لیست، یک Enumerable و یک آرایه را به صورت زیر مقدار دهی میکنیم و هر سهی اینها میتوانند نال هم باشند:
private IList<int>? _idsList; private IEnumerable<int>? _idsEnumerable; private int[]? _idsArray; [GlobalSetup] public void Setup() { _idsEnumerable = Enumerable.Range(0, 10000); _idsList = _idsEnumerable.ToList(); _idsArray = _idsEnumerable.ToArray(); }
اکنون که C# 9.0 در اختیار ما است به همراه pattern matching و همچنین Null Conditional Operator و غیره، میتوان روشهای زیر را برای بررسی خالی بودن این مجموعهها و آرایهها بکار گرفت:
1- استفاده از Null coalescing برای بررسی نال بودن مجموعه و سپس استفاده از متد Any برای بررسی خالی بودن آن:
var list = _idsList ?? new List<int>(); if (list.Any() is false) { }
2- استفاده از pattern matching برای بررسی نال بودن مجموعه و سپس استفاده از متد Any برای بررسی خالی بودن آن:
if (_idsList is null || _idsList.Any() is false) { }
3- استفاده از روش سنتی مقایسهی مستقیم با null و سپس استفاده از متد Any برای بررسی خالی بودن آن:
if (_idsList == null || _idsList.Any() is false) { }
4- استفاده از Null Conditional Operator برای بررسی نال بودن و سپس استفاده از متد Any برای بررسی خالی بودن آن:
if (_idsList?.Any() is false) { }
5- استفاده از pattern matching برای بررسی مقدار خاصیت Count یک لیست یا آرایه. البته Enumerableها به همراه این خاصیت نیستند و یا باید آنها را به لیست و یا آرایه تبدیل کرد و یا میتوان متد ()Count آنها را فراخوانی نمود:
if (_idsList is { Count: > 0 } is false) { }
6- استفاده از Null Conditional Operator برای بررسی نال بودن و سپس استفاده از مقدار خاصیت Count لیست، برای بررسی خالی بودن آن:
if (_idsList?.Count == 0) { }
7- استفاده از روش سنتی مقایسهی مستقیم با null و سپس استفاده از مقدار خاصیت Count لیست، برای بررسی خالی بودن آن:
if (_idsList == null || _idsList.Count == 0) { }
کدهای کامل این بررسی به صورت زیر هستند: AnyCountBenchmark.zip
ابتدا ارجاعی به BenchmarkDotNet به برنامه اضافه شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.12.1" /> </ItemGroup> </Project>
و سپس کدهای زیر، بررسی کارآیی روشهای مختلف تعیین خالی بودن مجموعهها را انجام میدهند:
using BenchmarkDotNet.Running; namespace AnyCountBenchmark { public static class Program { static void Main(string[] args) { #if DEBUG System.Console.WriteLine("Please set the project's configuration to Release mode first."); #else BenchmarkRunner.Run<Scenarios>(); #endif } } }
به همراه سناریوهای مختلف زیر:
using System; using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Order; namespace AnyCountBenchmark { [SimpleJob(RuntimeMoniker.NetCoreApp50)] [Orderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)] [RankColumn] public class Scenarios { private IList<int>? _idsList; private IEnumerable<int>? _idsEnumerable; private int[]? _idsArray; [GlobalSetup] public void Setup() { _idsEnumerable = Enumerable.Range(0, 10000); _idsList = _idsEnumerable.ToList(); _idsArray = _idsEnumerable.ToArray(); } #region Any_With_Null_coalescing [Benchmark] public void List_Any_With_Null_coalescing() { var list = _idsList ?? new List<int>(); if (list.Any() is false) { } } [Benchmark] public void Array_Any_With_Null_coalescing() { var array = _idsArray ?? Array.Empty<int>(); if (array.Any() is false) { } } [Benchmark] public void Enumerable_Any_With_Null_coalescing() { var enumerable = _idsEnumerable ?? Enumerable.Empty<int>(); if (enumerable.Any() is false) { } } #endregion #region Any_With_Is_Null_Check [Benchmark] public void List_Any_With_Is_Null_Check() { if (_idsList is null || _idsList.Any() is false) { } } [Benchmark] public void Array_Any_With_Is_Null_Check() { if (_idsArray is null || _idsArray.Any() is false) { } } [Benchmark] public void Enumerable_Any_With_Is_Null_Check() { if (_idsEnumerable is null || _idsEnumerable.Any() is false) { } } #endregion #region Any_Any_With_Null_Equality_Check [Benchmark] public void List_Any_With_Null_Equality_Check() { if (_idsList == null || _idsList.Any() is false) { } } [Benchmark] public void Array_Any_With_Null_Equality_Check() { if (_idsArray == null || _idsArray.Any() is false) { } } [Benchmark] public void Enumerable_Any_With_Null_Equality_Check() { if (_idsEnumerable == null || _idsEnumerable.Any() is false) { } } #endregion #region Any_With_Null_Conditional_Operator [Benchmark] public void List_Any_With_Null_Conditional_Operator() { if (_idsList?.Any() is false) { } } [Benchmark] public void Array_Any_With_Null_Conditional_Operator() { if (_idsArray?.Any() is false) { } } [Benchmark] public void Enumerable_Any_With_Null_Conditional_Operator() { if (_idsEnumerable?.Any() is false) { } } #endregion #region Count_With_Pattern_Matching [Benchmark] public void List_Count_With_Pattern_Matching() { if (_idsList is { Count: > 0 } is false) { } } [Benchmark] public void Array_Length_With_Pattern_Matching() { if (_idsArray is { Length: > 0 } is false) { } } [Benchmark] public void Enumerable_Count_With_Pattern_Matching() { var list = _idsEnumerable?.ToList(); if (list is { Count: > 0 } is false) { } } #endregion #region Count_With_Null_Conditional_Operator [Benchmark] public void List_Count_With_Null_Conditional_Operator() { if (_idsList?.Count == 0) { } } [Benchmark] public void Array_Length_With_Null_Conditional_Operator() { if (_idsArray?.Length == 0) { } } [Benchmark] public void Enumerable_Count_With_Null_Conditional_Operator() { if (_idsEnumerable?.Count() == 0) { } } #endregion #region Count_With_Null_Equality_Check [Benchmark] public void List_Count_With_Null_Equality_Check() { if (_idsList == null || _idsList.Count == 0) { } } [Benchmark] public void Array_Length_With_Null_Equality_Check() { if (_idsArray == null || _idsArray.Length == 0) { } } [Benchmark] public void Enumerable_Count_With_Null_Equality_Check() { if (_idsEnumerable == null || _idsEnumerable.Count() == 0) { } } #endregion } }
نتایج حاصل:
- بررسی خالی بودن آرایهها، بسیار سریعتر از بررسی خالی بودن لیستها و این مورد نیز سریعتر از Enumerableها است.
- اگر از آرایهها و یا لیستها استفاده میکنید، بررسی خاصیت Length و یا خاصیت Count آنها، بسیار سریعتر از بکارگیری متد Any بر روی آنها است.
- اگر از Enumerableها استفاده میکنید، استفاده از متد Any بر روی آنها، بسیار سریعتر از بکارگیری متد ()Count و یا تبدیل آنها به لیست و سپس بررسی خاصیت Count آنها است.
- بررسی نال بودن با pattern matching یا همان is null، نسبت به روشی سنتی استفادهی از null ==، سریعتر است. علت آنرا در مطلب «روش ترجیح داده شدهی مقایسه مقادیر اشیاء با null از زمان C# 7.0 به بعد» میتوانید مطالعه کنید.
بنابراین برای بررسی خالی بودن آرایهها و لیستها، بهتر است از خاصیت Length و یا Count آنها استفاده کرد و برای Enumerableها از متد ()Any.
.NET 7 Release Candidate 1 (RC1) is now available and includes many great new improvements to ASP.NET Core.
Here’s a summary of what’s new in this preview release:
- Dynamic authentication requests in Blazor WebAssembly
- Handle location changing events
- Blazor WebAssembly debugging improvements
- .NET WebAssembly build tools for .NET 6 projects
- .NET JavaScript interop on WebAssembly
- Kestrel full certificate chain improvements
- Faster HTTP/2 uploads
- HTTP/3 improvements
- Experimental Kestrel support for WebTransport over HTTP/3
- Experimental OpenAPI support for gRPC JSON transcoding
- Rate limiting middleware improvements
- macOS dev-certs improvements
- Accessibility improvements in narration, high contrast and focus control areas
- .NET Framework support for .NET Standard 2.0 and compiler features
- More secure SHA-2 support in ASP.NET and System.Messaging
- Configuration builders
- ASP.NET Execution step feature
- ASP.NET HttpCookie parsing
- Enhancements in Visual Tree for WPF applications
- Performance and reliability improvements
اشتراکها
NET 5.0 Preview 3. منتشر شد
اشتراکها
نگارش نهایی NET Core 2.1. منتشر شد
- Productivity and Performance improvements – Visual Studio is more responsive and usable, and projects load faster.
- Improved tooling to profile and understand your applications performance.
- Updated JavaScript and TypeScript tooling, including improved Vue.js and ESLint support, and right-click context menu productivity improvements.
- More C++ productivity improvements in IntelliSense, Code Analysis, and Just My Code debugging.
- Improved performance for Visual Basic integer manipulation.
- Azure Development improvements, including continuous delivery for Azure Functions, better experience managing secrets via Key Vault, and ability to configure Application Insights during initial site creation.
- More Library Manager features for managing Web Projects’ client-side library files.
- Mobile Development improvements such as faster Android incremental builds and inclusion of Xamarin.Essentials to facilitate building native apps.