عنوان:

‫مقایسه عملکرد ToListAsync و ToArrayAsync در Entity Framework: کدامیک سریع‌تر است؟


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۹/۲۵ ۱۰:۵۵
آدرس: www.dntips.ir
توسعه‌دهندگان اغلب با عملیات ناهمگام (آسنکرون) سروکار دارند که شامل واکشی داده‌ها از بانک اطلاعاتی است. یکی از سوالات رایج در این زمینه، مقایسه سرعت دو متد ToListAsync و ToArrayAsync است. این متدها برای تبدیل نتایج کوئری‌های IQueryable به مجموعه‌های قابل استفاده در حافظه به کار می‌روند. در این مقاله، به بررسی دلیل برتری عملکردی ToListAsync می‌پردازیم و با نگاهی به کد منبع، توضیح می‌دهیم چرا این تفاوت وجود دارد.

توضیح عملکرد و تحلیل کد
برای درک دقیق این مقایسه، ابتدا باید به کد منبع این متدها در Entity Framework Core نگاهی بیندازیم. این متدها بخشی از کلاس EntityFrameworkQueryableExtensions هستند و برای عملیات آسنکرون طراحی شده‌اند.
متد ToListAsync به صورت زیر تعریف شده است:
public static async Task<List<TSource>> ToListAsync<TSource>(
    this IQueryable<TSource> source,
    CancellationToken cancellationToken = default)
{
    var list = new List<TSource>();
    await foreach (var element in source.AsAsyncEnumerable().WithCancellation(cancellationToken).ConfigureAwait(false))
    {
        list.Add(element);
    }
    return list;
}
این متد یک لیست جدید از نوع List ایجاد می‌کند و سپس با استفاده از حلقه await foreach، عناصر را از منبع IQueryable (که به صورت آسنکرون به یک Enumerable تبدیل شده) واکشی و به لیست اضافه می‌کند. پارامتر CancellationToken برای مدیریت لغو عملیات اختیاری است و ConfigureAwait(false) برای جلوگیری از بازگشت به زمینه اصلی نخ (context) استفاده می‌شود، که این امر می‌تواند عملکرد را در سناریوهای آسنکرون بهبود بخشد.
در مقابل، متد ToArrayAsync به این شکل پیاده‌سازی شده:
public static async Task<TSource[]> ToArrayAsync<TSource>(
    this IQueryable<TSource> source,
    CancellationToken cancellationToken = default)
    => (await source.ToListAsync(cancellationToken).ConfigureAwait(false)).ToArray();
همان‌طور که مشاهده می‌کنید، ToArrayAsync مستقیماً ToListAsync را فراخوانی می‌کند و سپس نتیجه لیست را با استفاده از متد ToArray به آرایه تبدیل می‌نماید. این رویکرد دو مرحله‌ای منجر به overhead اضافی می‌شود: ابتدا یک لیست ایجاد و پر می‌شود، سپس یک آرایه جدید ساخته شده و عناصر از لیست به آن کپی می‌گردند. این عملیات کپی اضافی، تخصیص حافظه بیشتری مصرف می‌کند و زمان اجرای بیشتری می‌طلبد، به‌ویژه در مجموعه‌های بزرگ داده. این overhead نه تنها در زمان اجرا، بلکه در مدیریت حافظه نیز تأثیرگذار است. در دات‌نت، لیست‌ها (List) از آرایه‌های داخلی استفاده می‌کنند که می‌توانند به صورت پویا تغییر اندازه دهند، در حالی که آرایه‌ها (T[]) اندازه‌ی ثابت دارند. بنابراین، تبدیل لیست به آرایه، شامل تخصیص یک بلوک حافظه جدید و کپی عناصر است، که می‌تواند garbage collection را افزایش دهد و در برنامه‌های با بار کاری بالا مشکل‌ساز شود. آزمایش‌های عملی نشان می‌دهد که برای مجموعه‌های کوچک (کمتر از ۱۰۰۰ عنصر)، تفاوت ممکن است ناچیز باشد، اما در مقیاس‌های بزرگ‌تر، ToListAsync می‌تواند تا ۲۰-۳۰ درصد سریع‌تر عمل کند.

نکته‌ی مهم: این رفتار خاص Entity Framework Core است و در LINQ استاندارد (برای IEnumerable)، متفاوت عمل می‌کند. در LINQ، متدهای ToArray و ToList هر دو مستقیماً پیاده‌سازی می‌شوند؛ بدون وابستگی به یکدیگر، اما در نسخه‌ی آسنکرون Entity Framework، طراحی به گونه‌ای است که ToArrayAsync بر پایه‌ی ToListAsync ساخته شده تا کد تکراری کاهش یابد. این انتخاب طراحی، اولویت را به سادگی نگهداری کد می‌دهد، اما عملکرد را فدا می‌کند.

آیا موارد مشابه دیگری در دات‌نت وجود دارند؟ بله، برای مثال متد ToHashSetAsync در Entity Framework Core متفاوت عمل می‌کند و مستقیماً یک HashSet ایجاد می‌نماید بدون وابستگی به لیست یا آرایه. کد آن مشابه ToListAsync است اما از HashSet استفاده می‌کند:
public static async Task<HashSet<TSource>> ToHashSetAsync<TSource>(
    this IQueryable<TSource> source,
    IEqualityComparer<TSource>? comparer = null,
    CancellationToken cancellationToken = default)
{
    var set = comparer != null ? new HashSet<TSource>(comparer) : new HashSet<TSource>();
    await foreach (var element in source.AsAsyncEnumerable().WithCancellation(cancellationToken).ConfigureAwait(false))
    {
        set.Add(element);
    }
    return set;
}
این متد overhead کمتری دارد زیرا مستقیماً عناصر را به مجموعه اضافه می‌کند و برای سناریوهایی که نیاز به uniqueness (یگانه‌بودن) عناصر داریم، گزینه‌ی بهینه‌تری است. همچنین، در اکوسیستم دات‌نت، متدهایی مانند ToDictionaryAsync نیز وجود دارند که مستقیماً دیکشنری می‌سازند و می‌توانند در موارد خاص سریع‌تر از ToListAsync باشند، بسته به نیاز برنامه.

اهمیت تست عملکرد (benchmarking)
توسعه‌دهندگان نباید صرفاً به این تحلیل نظری اکتفا کنند؛ ابزارهایی مانند BenchmarkDotNet می‌توانند برای اندازه‌گیری دقیق زمان اجرا در محیط واقعی استفاده شوند. علاوه بر این، در سناریوهای پیچیده‌تر مانند کوئری‌های شامل joins یا projections، عوامل دیگری مانند بهینه‌سازی SQL تولیدشده توسط Entity Framework نیز نقش دارند.

نتیجه‌گیری
در نهایت، ToListAsync به دلیل پیاده‌سازی مستقیم و عدم نیاز به عملیات کپی اضافی، گزینه سریع‌تری نسبت به ToArrayAsync در Entity Framework Core است. این تفاوت ناشی از طراحی داخلی متدها است که ToArrayAsync را وابسته به ToListAsync می‌کند.