مجتبی
مجتبی
خواندن ۵ دقیقه·۲ سال پیش

بهبود عملکرد برنامه با استفاده از Span در #C

اگر تا به حال اسم این ساختارِ داده را در طول دوره‌ی برنامه‌نویسی‌تون با استفاده از زبان برنامه‌نویسی #C نشنیده‌اید، نگران نشوید؛ از این ساختار، عمدتا برای بهینه‌سازی استفاده از ظرفیت حافظه(RAM) و یا بهره‌گیری از امکانات تقسیم‌بندی(Slicing) داده استفاده می‌شود.

واژه‌ی Span آنطور که از لغت‌نامه‌های انگلیسی برمی‌آید، دو معنای پرکاربرد دارد:

  1. گستره‌ی زمانی برای انجام کاری
  2. مجموعه‌ یا گوناگونی‌ای از یک چیز

لازم است تا بدانیم که از حالا به بعد هنگامیکه از واژه‌ی Span استفاده می‌کنیم منظورمان معنی شماره ۲(مجموعه‌ یا گوناگونی‌ای از یک چیز) است.

در بخش بعدی به توضیح ساختارِ داده‌ی Span می‌پردازیم.


استفاده از Span چه مزایایی دارد؟

به طور کلی، دو مزیت اساسی می‌تواند برنامه‌نویس را به سوی استفاده از این ساختارِ داده سوق دهد:

  • شرایطی را فراهم می‌کند تا علاوه بر استفاده از ویژگی‌های آرایه، بدون نیاز به تولید کُدهای تکراری و درگیر شدن با pointerها، بتوان با استفاده از فضای حافظه‌ی stack و مدیریت‌نشده، از بروز بازیافت حافظه یا Garbage Collection جلوگیری کرد.
  • در مواقعی که نیاز است تا بخشی از مجموعه را بدون کُپی‌کردن برای انجام کاری جداسازی یا تقسیم کنیم.

با کمی دقت درمی‌یابیم که هر دو مورد گفته‌شده، شاهد استفاده‌ی بهینه‌تر از حافظه هستیم. در مورد اول، Compiler را از انجام Garbage Collectionهای بی‌رویه بی‌نیاز کردیم و در مورد دوم هم از اشغال‌کردن حافظه‌ برای نگهداری داده‌های تکراری جلوگیری کرده‌ایم.

در ادامه در بخش تست عملکرد، مقایسه‌ای از مصرف منابع یک عملیات خاص در حالت عادی با حالتی که از Span استفاده شده را مشاهده می‌کنیم تا این بهینه‌سازی بیشتر قابل درک شود.


ساختارِ داده‌ی Span

اگر به منبع کد Span مراجعه کنید با این تعریف مواجه می‌شوید:

“Span represents a contiguous region of arbitrary memory. Unlike arrays, it can point to either managed or native memory, or to memory allocated on the stack. It is type- and memory-safe.”

یعنی این ساختارِ داده می‌تواند بخشی از حافظه‌ی دلخواه پیوسته را در بر بگیرد. برخلاف آرایه‌ها، می‌تواند به تکه‌ای از حافظه‌ی مدیریت‌شده، حافظه‌ی مدیریت‌نشده و یا حافظه‌ی تخصیص‌یافته اشاره کند. علاوه بر این؛ از لحاظ نوع و حافظه امن است.

در نظر داشته باشید که این ساختار داده همانند آرایه‌ها دربردارنده‌ی مجموعه‌ای از المان‌های یک نوعِ داده است. بنابراین تعیین نوع داده برای ایجاد نمونه‌ای از Span لازم است. به طور مثال اگر می‌خواهید آرایه‌ای از نوع عددیِ Integer را توسط Span احاطه کنید، تعریف آن به صورت زیر خواهد بود:

Span<int> arraySpan;

از آنجایی که Span در #C با مشخصه‌ی ref struct تعریف شده است، یک ساختار داده‌ی نوع ارجاعی(Reference-typed) است و نمی‌توان آن را در شرایط زیر نگهداری کرد:

  • به عنوان یک فیلد در یک کلاس(این کار منجر به انتقال آن به حافظه‌ی heap خواهد شد و Compiler خطا می‌دهد).
  • به عنوان ورودی در متدهای async
  • در عبارات Lambda
  • در پیمایشگرها(Iterators)
  • در جریان‌های asynchronous

باید توجه کنیم که Compiler در خلال پردازش متدهای asynchronous و پیمایشگرها با ایجاد یک state machine، ورودی‌ها و متغیرهای محلی را به عنوان یک فیلد نگهداری می‌کند. همین اتفاق در رابطه با عبارات Lambda هم می‌افتد. به همین دلیل ۴ مورد ذکر شده، محل نگهداری مناسبی برای Spanها نیستند.

به لطف implicit operatorهایی که برای Span تعریف شده، به راحتی می‌توان محتویات []T را برای متغیری با نوع <Span<T جایگزینی کرد:

int[] integerArray = new int[] { 1, 2, 3, 4, 5, 6 }; Span<int> integerSpan = integerArray;

گفتنی است درباره‌ی اشیاء از نوع String، این جایگزینی به شکل زیر ممکن است. چرا که String مجموعه‌ای از المان‌های از نوع char به حساب می‌آید:

string str = &quotI am a String&quot Span<char> strSpan = str;

ساختارِ داده‌ی Span یک ساختارِ داده‌ی همتا به نام ReadOnlySpan هم دارد که مشخصا برای کاربردهای فقط خواندنی مورد استفاده قرار می‌گیرد. تمام ویژگی‌های گفته شده در مورد Span، در آن صدق می‌کند؛ با این تفاوت که المان‌های حاضر در این ساختارِ داده، قابل تغییر نیستند.


تست عملکرد

برای درک بهتر تاثیر استفاده از Span در عملکرد برنامه، یک عملیات خاص را در دو شرایط مختلف مورد آزمایش قرار دادم تا با رجوع به نتایج، نسبت به عملکرد این ساختارِ داده دید بهتری داشته باشیم. عملیاتی که در دو شرایط مورد آزمایش قرار داده شد، کوچک‌سازی حروف کلمات یک پاراگراف شامل حروف انگلیسی است.

نتیجه‌ی ردیف اول، حاصل انجام عملیات کوچک‌سازی بر روی پاراگرافی با نوع String و با فراخوانی متد ()ToLower است.

نتیجه‌ی ردیف دوم، حاصل انجام عملیات کوچک‌سازی با فراخوانی متد ()ToLower است؛ اما این متد، بر روی متغیری با نوع <ReadOnlySpan<char فراخوانی شده است.

در نتایج دو اندازه در ستون‌های دوم و سوم قابل مشاهده است. ستون دوم(Mean) برابر است با زمانی که صرف انجام عملیات شده و ستون سوم(Allocated) برابر است با ظرفیتی از حافظه که در طول عملیات اشغال شده است.

همانطور که از نتایج پیداست:

  • هزینه‌ی زمانی‌ای که صرف عملیات شده در حالتی که از Span استفاده نشده است، تقریبا ۷۲/۵ برابر حالتی است که استفاده شده است.
  • هیچ حافظه‌‌ای در حالتی که از Span استفاده شده است، اشغال نشده است. این در حالیست که در حالت دیگر، مقداری از حافظه درگیر انجام عملیات بوده است.

جمع‌بندی

در کنار اصلی‌ترین مزیت استفاده از Span که بهینه‌سازی در عملکرد برنامه است، نباید معایبش را از یاد برد. همانطور که گفته شد، نمی‌توان این ساختارِ داده را در هر شرایطی نگهداری و استفاده کرد و باید دانست که در بعضی از عملیات، لازم است تا از ساختارِ داده‌های دیگری مثل Memory بهره گرفت. به عنوان مثال، از آنجا که Span نمی‌تواند در تشکیل یک آرایه به کار گرفته شود، بنابراین نمی‌توان از آن برای عملیات Split کردن یک رشته استفاده کرد.

بنابراین شاید نیاز باشد تا برای کسب نتیجه‌ی دلخواه‌تان، پیچیدگی بیشتری به کُد برنامه‌تان اضافه کنید. به نظر من این کار زمانی منطقی به نظر می‌رسد که میزان عملکرد و پاسخگویی برنامه برایتان از همه چیز مهم‌تر باشد. در غیر اینصورت، اضافه‌کردن پیچیدگی بیشتر به برنامه به ضرر شما و بقیه‌ی برنامه‌نویسان است.

سخن پایانی

از شما برای زمانی که صرف خواندن این مقاله کردید صمیمانه سپاسگزارم و باعث خشنودی من خواهید شد اگر درباره‌ی کیفیت و موضوع متن نقطه‌نظر یا انتقادی دارید من را مطلع کنید.

csharpperformancegarbage collection
CODE IS CO$T
شاید از این پست‌ها خوشتان بیاید