نوعهای جدید <Span<T و <ReadOnlySpan<T در C# 7.2
نوعهای جدید <Span<T و <ReadOnlySpan<T جهت ارائهی ناحیههای اختیاری پیوستهای از حافظه، شبیه به آرایهها تدارک دیده شدهاند و هدف استفادهی از آنها، تولید برنامههای سمت سرور با کارآیی بالا است.
برای کار با این نوعها، هم نیاز به کامپایلر C# 7.2 است و هم نصب بستهی نیوگت System.Memory:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.0</TargetFramework> <LangVersion>latest</LangVersion> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Memory" Version="4.4.0-preview1-25305-02" /> </ItemGroup> </Project>
Spanها و امکان دسترسی به انواع حافظه
Spanها میتوانند به حافظهی مدیریت شده، حافظهی بومی (native) و حافظهی اختصاص داده شدهی در Stack اشاره کنند. به عبارتی Spanها یک لایه انتزاعی، برفراز تمام انواع و اقسام حافظههایی هستند که میتوانند در اختیار توسعه دهندگان NET. باشند.
- البته اکثر توسعه دهندگان دات نت از حافظهی مدیریت شده استفاده میکنند. برای مثال Stack memory تنها از طریق کدهای unsafe و واژهی کلیدی stackalloc قابل تخصیص است. این نوع حافظه بسیار سریع است و همچنین بسیار کوچک؛ کمتر از یک مگابایت که به خوبی در CPU cache جا میشود. اما اگر در این بین حجم حافظهی تخصیصی بیشتر از یک مگابایت شود، بلافاصله استثنای StackOverflowException غیرقابل مدیریتی را به همراه خاتمهی فوری برنامه به همراه خواهد داشت. برای نمونه از این نوع حافظه در جهت مدیریت رخدادهای داخلی corefx زیاد استفاده میشود.
- حافظهی مدیریت شده، همان حافظهای است که توسط واژهی کلیدی new در برنامه، جهت ایجاد اشیاء، تخصیص داده میشود و طول عمر آن تحت مدیریت GC است.
- حافظهی مدیریت نشده یا بومی از دید GC مخفی است و توسط متدهایی مانند Marshal.AllocHGlobal و Marshal.AllocCoTaskMem در اختیار برنامه قرار میگیرند. این حافظه باید به صورت صریحی توسط توسعه دهنده به کمک متدهایی مانند Marshal.FreeHGlobal و Marshal.FreeCoTaskMem آزاد شود. وب سرور Kestrel مخصوص ASP.NET Core، از این روش جهت کار با آرایههای حجیم، جهت کاهش بار GC استفاده میکند.
مزیت کار با Spanها این است که دسترسی امن و type safeایی را به انواع حافظههای مهیا، جهت توسعه دهندگانی که عموما کدهای unsafe ایی را نمینویسند و با اشارهگرها به صورت مستقیم کار نمیکنند، میسر میکند. برای مثال تا پیش از معرفی Spanها، برای دسترسی به 1000 عنصر یک آرایهی 10 هزار عنصری و ارسال آن به یک متد، نیاز بود تا ابتدا یک کپی از این 1000 عنصر را تهیه کرد. این عملیات از لحاظ میزان مصرف حافظه و همچنین زمان انجام آن، بسیار هزینهبر است. با استفاده از <Span<T میتوان یک دید مجازی از آن آرایه را بدون اختصاص آرایهای و یا آرایههایی جدید، ارائه کرد.
مثالی از کاربرد Spanها جهت کاهش تعداد بار تخصیصهای حافظه
برای نمونه، متد IsValidName زیر، بررسی میکند که طول رشتهی دریافتی حداقل 2 باشد و حتما با یک حرف شروع شده باشد:
static class NameValidatorUsingString { public static bool IsValidName(string name) { if (name.Length < 2) return false; if (char.IsLetter(name[0])) return true; return false; } }
string fullName = "User 1"; string firstName = fullName.Substring(0, 4); NameValidatorUsingString.IsValidName(firstName);
همچنین اگر این اطلاعات را از طریق شبکه دریافت کرده باشیم، ممکن است به صورت آرایهای از حروف دریافت شوند:
char[] anotherFullName = { 'A', 'B' };
NameValidatorUsingString.IsValidName(anotherFullName);
NameValidatorUsingString.IsValidName(new string(anotherFullName));
اکنون در C# 7.2، بازنویسی این متد توسط ReadOnlySpan، به صورت ذیل است:
static class NameValidatorUsingSpan { public static bool IsValidName(ReadOnlySpan<char> name) { if (name.Length < 2) return false; if (char.IsLetter(name[0])) return true; return false; } }
ReadOnlySpan<char> fullName = "User 1".AsSpan(); ReadOnlySpan<char> firstName = fullName.Slice(0, 4); NameValidatorUsingSpan.IsValidName(firstName);
و یا اینبار امکان استفادهی از آرایهای از کاراکترها، بدون نیاز به تخصیص حافظهای جدید، برای بررسی اعتبار مقادیر دریافتی میسر است:
char[] anotherFullName = { 'A', 'B' }; NameValidatorUsingSpan.IsValidName(anotherFullName);
برای نمونه از یک چنین APIایی در پشت صحنهی کتابخانههایی مانند SignalR و یا Roslyn، برای بالا بردن کارآیی برنامه، با کاهش تعداد بار تخصیصهای حافظهی مورد نیاز، بسیار استفاده شدهاست. برای نمونه در NET Core 2.1.، حجم رشتههای تخصیص داده شدهی در فریم ورکهای وابسته، به این ترتیب به شدت کاهش یافتهاست.
مثالهایی از کار با API نوع Span
امکان ایجاد یک Span از یک array
var arr = new byte[10]; Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>
Span<byte> slicedBytes = bytes.Slice(start: 5, length: 2); slicedBytes[0] = 42; slicedBytes[1] = 43; slicedBytes[2] = 44; // Throws IndexOutOfRangeException bytes[2] = 45; // OK
همچنین تغییرات بر روی Span (غیر read only) بر روی آرایهی اصلی نیز تاثیر گذار است. برای مثال در اینجا با تغییر bytes[2]، مقدار arr[2] نیز تغییر میکند.
و یا روش دیگر ایجاد Span استفاده از متد AsSpan است:
var array = new byte[100]; Span<byte> interiorRef1 = array.AsSpan().Slice(start: 20);
Span<byte> interiorRef2 = new Span<byte>(array: array, start: 20, length: array.Length - 20);
محدودیتهای کار با Spanها
- Span تنها یک نوع stack-only است.
- Spanها در بین تردها به اشتراک گذاشته نمیشوند. هر استک در یک زمان تنها توسط یک ترد قابل دسترسی است. بنابراین Spanها thread-safe هستند.
- طول عمر Spanها کوتاه است و قابلیت قرارگیری بر روی heap با طول عمر بیشتر را ندارند؛ یعنی:
- به صورت فیلد در یک نوع non-stackonly قابل تعریف نیستند:
class Impossible { Span<byte> field; }
- به عنوان پارامترهای متدهای async قابل استفاده نیستند. چون در این بین در پشت صحنه یک AsyncMethodBuilder تشکیل میشود که در قسمتی از آن، پارامترها بر روی heap قرار میگیرند.
- هرجائیکه عملیات boxing صورت گیرد، نتیجهی عملیات بر روی heap قرار میگیرد. بنابراین در یک چنین مواردی نمیتوان از Spanها استفاده کرد. برای مثال تعریف Func<T> valueProvider و سپس فراخوانی ()valueProvider.Invoke به همراه یک boxing است. بنابراین نمیتوان از spanها به عنوان نوع آرگومان جنریک استفاده کرد. این مورد هرچند کامپایل میشود، اما در زمان اجرا سبب خاتمهی برنامه خواهد شد و یا نمونهی دیگر، عدم امکان دسترسی به آنها توسط reflection invoke APIs است که سبب boxing میشود.
معرفی نوع <Memory<T
با توجه به محدودیتهای Span و خصوصا اینکه به عنوان پارامتر متدهای async قابل استفاده نیست (چون بر روی stack ذخیره میشوند)، نوع دیگری به نام <Memory<T نیز به همراه C# 7.2 ارائه شدهاست. البته این نوع هنوز به بستهی نیوگت فوق اضافه نشدهاست و به همراه ارائه نهایی NET Core 2.1. ارائه خواهد شد.
این نوع، محدودیت <Span<T را نداشته و قابلیت ذخیره سازی بر روی heap را دارا است.
static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream) { int bytesRead = await stream.ReadAsync(buffer); return Checksum(buffer.Span.Slice(0, bytesRead)); // Or buffer.Slice(0, bytesRead).Span }