کاهش کارایی به دلیل استفاده از abstractions در داتنت
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۱۲ ۰۸:۱۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
IList یا کلاسهای انتزاعی پیادهسازی میشوند. بااینحال، استفاده از این abstractionها گاهی اوقات میتواند به کاهش کارایی (Performance) منجر شود. این مقاله به بررسی دلایل کاهش کارایی هنگام استفاده از abstractionها در داتنت، بهویژه در مقایسه بین List و IList، میپردازد. با تمرکز بر مفاهیمی مانند تخصیص حافظه (Memory Allocation) و فراخوانیهای متد مجازی (Virtual Method Calls)، این مقاله به توسعهدهندگان کمک میکند تا تصمیمات آگاهانهتری در طراحی کدهای خود بگیرند. IList، به توسعهدهندگان اجازه میدهند تا بدون وابستگی به پیادهسازی خاص یک کلاس، با مجموعهای از دادهها کار کنند. این انعطافپذیری در طراحی نرمافزار بسیار ارزشمند است، اما هزینهای در بر دارد. یکی از مهمترین دلایل کاهش کارایی هنگام استفاده از abstractionها، تفاوت در نحوه مدیریت حافظه و فراخوانی متدها بین یک نوع مشخص (Concrete Type) مانند List و یک رابط مانند IList است. برای درک بهتر این موضوع، بیایید به یک مثال عملی بپردازیم که این تفاوتها را نشان میدهد.List و یک IList را با استفاده از حلقه foreach بررسی میکند. کد زیر با استفاده از ابزار بنچمارک (BenchmarkDotNet) نوشته شده است:[MemoryDiagnoser]
public class Benchmark
{
private readonly List<int> numbersList = Enumerable.Range(0, 10_000).ToList();
private readonly IList<int> numbersIList = Enumerable.Range(0, 10_000).ToList();
[Benchmark(Baseline = true)]
public int GetSumOfList()
{
var sum = 0;
foreach (var number in numbersList) { sum += number; }
return sum;
}
[Benchmark]
public int GetSumOfIList()
{
var sum = 0;
foreach (var number in numbersIList) { sum += number; }
return sum;
}
}List بهطور قابلتوجهی سریعتر از IList است. میانگین زمان اجرای متد GetSumOfList حدود 4.214 میکروثانیه است، درحالیکه متد GetSumOfIList حدود 19.508 میکروثانیه طول میکشد. این تفاوت تقریباً 4.63 برابر است. همچنین، پیمایش روی IList منجر به تخصیص 40 بایت حافظه اضافی میشود، درحالیکه برای List هیچ تخصیص حافظهای مشاهده نمیشود. اما چرا این تفاوتها وجود دارند؟IList، تخصیص حافظه اضافی (Memory Allocation) است. هنگامی که حلقه foreach روی یک List اجرا میشود، متد GetEnumerator() یک ساختار (Struct) به نام Enumerator بازمیگرداند. از آنجا که ساختارها در داتنت بهصورت مقدار (Value Type) ذخیره میشوند، نیازی به تخصیص حافظه در هیپ (Heap) ندارند. اما وقتی foreach روی یک IList اجرا میشود، متد GetEnumerator() یک نمونه از نوع IEnumerator بازمیگرداند که در واقع یک نسخه باکسشده (Boxed) از Enumerator است. فرآیند باکسینگ (Boxing) شامل تبدیل یک نوع مقدار به یک نوع مرجع (Reference Type) است که نیاز به تخصیص حافظه در هیپ دارد. این تخصیص حافظه اضافی، همان 40 بایت گزارششده در نتایج بنچمارک است.IList استفاده میکنید، متدهایی مانند GetEnumerator() بهصورت مجازی فراخوانی میشوند. این به این معناست که در زمان اجرا، سیستم باید نوع واقعی (Concrete Type) شیء را بررسی کند و متد مناسب را فراخوانی کند. این فرآیند، که به نام توزیع پویا (Dynamic Dispatch) شناخته میشود، نسبت به فراخوانی مستقیم متدهای غیرمجازی (Non-Virtual Methods) کندتر است. در مقابل، وقتی مستقیماً با List کار میکنید، نیازی به بررسی نوع در زمان اجرا نیست، زیرا List خود یک نوع مشخص است و متدهای آن بهصورت مستقیم فراخوانی میشوند.List بهجای رابطهای عمومی مانند IList میتواند مفید باشد. ثانیاً، میتوان از تکنیکهایی مانند حلقههای for بهجای foreach استفاده کرد تا کنترل بیشتری بر فرآیند پیمایش داشته باشیم. برای مثال، در کد زیر، استفاده از حلقه for میتواند تخصیص حافظه را به حداقل برساند:public int GetSumWithForLoop()
{
var sum = 0;
for (int i = 0; i < numbersList.Count; i++)
{
sum += numbersList[i];
}
return sum;
}Enumerator جلوگیری میکند.List بهجای کلاسهای مشخص مانند ArrayList میتواند مشکلات مشابهی ایجاد کند. این موضوع نشاندهنده یک اصل کلیتر در طراحی نرمافزار است: تعادل بین انعطافپذیری و کارایی. توسعهدهندگان باید هنگام انتخاب بین abstraction و انواع مشخص، نیازهای پروژه را بهدقت ارزیابی کنند. اگر انعطافپذیری و قابلیت نگهداری کد اولویت دارد، استفاده از رابطها منطقی است. اما در بخشهای حساس به کارایی، استفاده از انواع مشخص میتواند انتخاب بهتری باشد.List سریعتر از IList است؟List از متدهای مجازی استفاده میکند.List نیازی به تخصیص حافظه اضافی ندارد و فراخوانیهای غیرمجازی انجام میدهد.IList از ساختار (Struct) بهجای نوع مرجع استفاده میکند.List دادهها را در حافظه کش (Cache) ذخیره میکند. IList، انعطافپذیری و قابلیت نگهداری کد را افزایش میدهد، اما میتواند به کاهش کارایی منجر شود. این کاهش کارایی عمدتاً به دلیل تخصیص حافظه اضافی ناشی از باکسینگ (Boxing) و فراخوانیهای متد مجازی است. با درک این مفاهیم و استفاده از استراتژیهایی مانند انتخاب انواع مشخص در بخشهای حساس به کارایی یا استفاده از حلقههای for، توسعهدهندگان میتوانند تعادل بهتری بین انعطافپذیری و کارایی برقرار کنند. این دانش به برنامهنویسان داتنت کمک میکند تا کدهایی کارآمدتر و بهینهتر بنویسند و از مشکلات پنهان عملکردی جلوگیری کنند.List نیازی به تخصیص حافظه اضافی ندارد و فراخوانیهای غیرمجازی انجام میدهد.