عنوان:

‫کاهش کارایی به دلیل استفاده از abstractions در دات‌نت


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۱۲ ۰۸:۱۵
آدرس: www.dntips.ir
استفاده از abstractionها (Abstractions) یکی از اصول کلیدی طراحی نرم‌افزار است و به توسعه‌دهندگان امکان می‌دهد کدهایی منعطف‌تر، قابل‌نگهداری و ماژولار بنویسند. در چارچوب دات‌نت (Microsoft .NET Framework)، abstractionها اغلب به شکل رابط‌ها (Interfaces) مانند IList یا کلاس‌های انتزاعی پیاده‌سازی می‌شوند. بااین‌حال، استفاده از این abstractionها گاهی اوقات می‌تواند به کاهش کارایی (Performance) منجر شود. این مقاله به بررسی دلایل کاهش کارایی هنگام استفاده از abstractionها در دات‌نت، به‌ویژه در مقایسه بین List و IList، می‌پردازد. با تمرکز بر مفاهیمی مانند تخصیص حافظه (Memory Allocation) و فراخوانی‌های متد مجازی (Virtual Method Calls)، این مقاله به توسعه‌دهندگان کمک می‌کند تا تصمیمات آگاهانه‌تری در طراحی کدهای خود بگیرند.

چرا استفاده از abstractionها کارایی را کاهش می‌دهد؟
abstractionها در دات‌نت، مانند رابط IList، به توسعه‌دهندگان اجازه می‌دهند تا بدون وابستگی به پیاده‌سازی خاص یک کلاس، با مجموعه‌ای از داده‌ها کار کنند. این انعطاف‌پذیری در طراحی نرم‌افزار بسیار ارزشمند است، اما هزینه‌ای در بر دارد. یکی از مهم‌ترین دلایل کاهش کارایی هنگام استفاده از abstractionها، تفاوت در نحوه مدیریت حافظه و فراخوانی متدها بین یک نوع مشخص (Concrete Type) مانند List و یک رابط مانند IList است. برای درک بهتر این موضوع، بیایید به یک مثال عملی بپردازیم که این تفاوت‌ها را نشان می‌دهد.

بررسی کد نمونه
برای مقایسه کارایی، کدی را در نظر بگیرید که عملکرد پیمایش (Enumeration) روی یک 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 بایت گزارش‌شده در نتایج بنچمارک است.

فراخوانی‌های متد مجازی
دلیل دوم کاهش کارایی، استفاده از فراخوانی‌های متد مجازی (Virtual Method Calls) است. وقتی از رابط IList استفاده می‌کنید، متدهایی مانند GetEnumerator() به‌صورت مجازی فراخوانی می‌شوند. این به این معناست که در زمان اجرا، سیستم باید نوع واقعی (Concrete Type) شیء را بررسی کند و متد مناسب را فراخوانی کند. این فرآیند، که به نام توزیع پویا (Dynamic Dispatch) شناخته می‌شود، نسبت به فراخوانی مستقیم متدهای غیرمجازی (Non-Virtual Methods) کندتر است. در مقابل، وقتی مستقیماً با List کار می‌کنید، نیازی به بررسی نوع در زمان اجرا نیست، زیرا List خود یک نوع مشخص است و متدهای آن به‌صورت مستقیم فراخوانی می‌شوند.

تأثیرات در دنیای واقعی
تفاوت‌های ذکرشده ممکن است در برنامه‌های کوچک یا با داده‌های کم‌حجم ناچیز به نظر برسند، اما در سناریوهای واقعی که شامل مجموعه‌های بزرگ داده یا حلقه‌های تکراری متعدد هستند، این تفاوت‌ها می‌توانند به کاهش قابل‌توجه کارایی منجر شوند. برای مثال، در برنامه‌های سرورمحور یا اپلیکیشن‌هایی با پردازش سنگین، مانند پردازش داده‌های بلادرنگ، حتی چند میکروثانیه تأخیر می‌تواند تأثیرات قابل‌توجهی داشته باشد. علاوه بر این، تخصیص‌های مکرر حافظه می‌توانند فشار بیشتری به جمع‌کننده زباله (Garbage Collector) وارد کنند و باعث توقف‌های موقتی در اجرای برنامه شوند.

راهکارهای بهبود کارایی
برای کاهش تأثیرات منفی استفاده از abstractionها، توسعه‌دهندگان می‌توانند چند استراتژی را به کار گیرند. اولاً، در بخش‌هایی از کد که کارایی حیاتی است، استفاده از انواع مشخص مانند 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 جلوگیری می‌کند.

ارتباط با مفاهیم دیگر
مفهوم کاهش کارایی به دلیل abstractionها نه‌تنها در دات‌نت، بلکه در سایر زبان‌های برنامه‌نویسی مانند جاوا یا ++C نیز دیده می‌شود. به عنوان مثال، در جاوا، استفاده از رابط‌هایی مانند List به‌جای کلاس‌های مشخص مانند ArrayList می‌تواند مشکلات مشابهی ایجاد کند. این موضوع نشان‌دهنده یک اصل کلی‌تر در طراحی نرم‌افزار است: تعادل بین انعطاف‌پذیری و کارایی. توسعه‌دهندگان باید هنگام انتخاب بین abstraction و انواع مشخص، نیازهای پروژه را به‌دقت ارزیابی کنند. اگر انعطاف‌پذیری و قابلیت نگهداری کد اولویت دارد، استفاده از رابط‌ها منطقی است. اما در بخش‌های حساس به کارایی، استفاده از انواع مشخص می‌تواند انتخاب بهتری باشد.

خودآزمایی
برای درک بهتر مطالب ارائه‌شده، سؤال زیر را پاسخ دهید:
سؤال: چرا پیمایش روی List سریع‌تر از IList است؟
الف) چون List از متدهای مجازی استفاده می‌کند.
ب) چون List نیازی به تخصیص حافظه اضافی ندارد و فراخوانی‌های غیرمجازی انجام می‌دهد.
ج) چون IList از ساختار (Struct) به‌جای نوع مرجع استفاده می‌کند.
د) چون List داده‌ها را در حافظه کش (Cache) ذخیره می‌کند.


نتیجه‌گیری
استفاده از abstractionها در دات‌نت، مانند رابط IList، انعطاف‌پذیری و قابلیت نگهداری کد را افزایش می‌دهد، اما می‌تواند به کاهش کارایی منجر شود. این کاهش کارایی عمدتاً به دلیل تخصیص حافظه اضافی ناشی از باکسینگ (Boxing) و فراخوانی‌های متد مجازی است. با درک این مفاهیم و استفاده از استراتژی‌هایی مانند انتخاب انواع مشخص در بخش‌های حساس به کارایی یا استفاده از حلقه‌های for، توسعه‌دهندگان می‌توانند تعادل بهتری بین انعطاف‌پذیری و کارایی برقرار کنند. این دانش به برنامه‌نویسان دات‌نت کمک می‌کند تا کدهایی کارآمدتر و بهینه‌تر بنویسند و از مشکلات پنهان عملکردی جلوگیری کنند.


پاسخ خودآزمایی
پاسخ صحیح: ب) چون List نیازی به تخصیص حافظه اضافی ندارد و فراخوانی‌های غیرمجازی انجام می‌دهد.


مشاهده مطلب اصلی