اگر بخواهیم یک لیستی از داده ها را نگهداری کنیم، اولین راهی که به ذهنمان میرسد List یا Array است. در این مطلب میخواهیم یک نکته در مورد فضای اشغال شده توسط List را بررسی کنیم. کلاس List از یک آرایه داخلی برای نگهداری عناصر استفاده میکند. زمانی که یک List را نمونه سازی میکنید، اگر سازنده پیشفرض آن استفاده کنید، در اولین عملیات اضافه کردن عنصر، آرایه داخلی کلاس List با 4 عنصر ایجاد میشود و هر زمانی که یک عنصر جدید به List اضافه میکنید، ابتدا بررسی میکند که ظرفیت آرایه داخلی به حداکثر ظرفیت خود رسیده است یا نه؟ اگر به حداکثر مقدار رسیده باشد، یک آرایه جدید با طول دو برابر آرایه قبلی ایجاد میکند و تمامی عناصر آرایه قبلی را درون آرایه جدید کپی میکند و سپس عنصر جدید را به آرایه اضافه میکند. در کد زیر وضعیت آرایه داخلی کلاس List را نشان میدهد:

var list = new List<char>();   // []   Count:    0  Capacity: 0

list.Add('h');                 // ['h', null, null, null] 	Count: 1	Capacity: 4

list.Add('e');                 // ['h', 'e', null, null] 	Count: 2	Capacity: 4

list.Add('l');                 // ['h', 'e', 'l', null]     Count: 3	Capacity: 4
 
list.Add('l');                 // ['h', 'e', 'l', 'l']   	Count: 4	Capacity: 4  

list.Add('o');                 // ['h', 'e', 'l', 'l', 'o' , null, null, null]   Count:5	Capacity: 8

list.Add(' ');                 // ['h', 'e', 'l', 'l', 'o' , ' ', null, null]    Count:6	Capacity: 8

list.Add('w');                 // ['h', 'e', 'l', 'l', 'o' , ' ', 'w', null]     Count:7	Capacity: 8

list.Add('o');                 // ['h', 'e', 'l', 'l', 'o' , ' ', 'w', 'o']      Count:8	Capacity: 8

list.Add('r');                 // ['h', 'e', 'l', 'l', 'o' , ' ', 'w', 'o', 'r', null, null, null, null, null, null, null]   Count: 9	Capacity: 16

list.Add('l');                 // ['h', 'e', 'l', 'l', 'o' , ' ', 'w', 'o', 'r', 'l', null, null, null, null, null, null]    Count: 10	Capacity: 16

list.Add('d');                 // ['h', 'e', 'l', 'l', 'o' , ' ', 'w', 'o', 'r', 'l', 'd', null, null, null, null, null]     Count: 11	Capacity: 16

همانطور که مشاهده میکنید، زمانی که به حداکثر ظرفیت آرایه میرسد، آرایه جدید با طول دو برابر آرایه قبلی ایجاد میشود 4 > 8 > 16 > 32 > 64 > 128 ...

اما اگر در همان اول ساخت کلاس List فضای موردنیاز برای افزودن عناصر را مشخص کنید به صورت زیر:

var list = new List<char>(11);

 هزینه نمونه سازی جدید آرایه و کپی کردن را از بین میبرید و عملا از لحاظ پرفورمنسی بهتر خواهد بود. 

نکته دوم درمورد حذف کردن آیتم های یک لیست است: برخلاف اضافه کردن عنصر که اندازه آرایه تغییر میکند، حذف کردن یک عنصر از لیست فضای اشغال شده را آزاد نمیکند. برای حذف فضای اشغال شده میتوانید متد TrimExcess را فراخوانی کنید. در کد زیر دیتای متغییری که لیست درون آن قرار دارد Clear میشود ولی فضای اشغال شده هنوز پابرجاست:

List<int> numbers = new List<int>();
numbers.Add(5);
numbers.Add(1);
numbers.Add(3);
numbers.Add(2);
numbers.Add(0);
Console.WriteLine(numbers.Capacity);
numbers.Clear();
Console.WriteLine(numbers.Capacity);
numbers.TrimExcess();
Console.WriteLine(numbers.Capacity);

در صفحه کنسول مقادیر زیر را مشاهده میکنید:

8
8
0

که بعد از متد Clear فضای اشغال شده درون آرایه هنوز پاک نشده است و همان 8 را نشان میدهد. اما بعد از متد TrimExcess مقدار پراپرتی Capacity به 0 تغییر میکند. بنابراین اگر میخواهید فضای اشغال شده را پاک کنید، ابتدا باید متد Clear و بعد از آن TrimExcess را فراخوانی کنید.

برای بررسی این مورد از کتابخانه Benchmark استفاده کردیم:

[MemoryDiagnoser]
public class ListMemoryAllocationTest
{
    public int Count { get; set; } = 100000;
    [Benchmark]
    public  void DefaultConstructor()
    {
        List<int> numbers = new List<int>();
        for (int i = 0; i < Count; i++)
        {
            numbers.Add(i);
        }
    }
    [Benchmark]
    public  void AddCountToConstructor()
    {
        List<int> numbers = new List<int>(Count);
        for (int i = 0; i < Count; i++)
        {
            numbers.Add(i);
        }
    }
}
public class Program
{
    public static void Main()
    {
        BenchmarkRunner.Run<ListMemoryAllocationTest>();
    }
}

نتیجه Benchmark:

|                Method |     Mean |    Error |   StdDev |    Gen 0 |    Gen 1 |    Gen 2 | Allocated |

|    DefaultConstructor | 753.4 us | 14.80 us | 15.20 us | 628.9063 | 627.9297 | 237.3047 |  1,024 KB |
| AddCountToConstructor | 499.6 us |  9.86 us | 17.78 us | 255.8594 | 255.8594 |  95.7031 |    391 KB |

زمانی که از سازنده پیشفرض کلاس List استفاده میشود حافظه بیشتری اشغال میشود.

:)

Powered by Froala Editor

نظرات


سایر نظرات
  • کاربر مهمان

    سلام و درودبسیار عالیممنون

    ارسال شده در تاریخ شنبه، ۲۰ شهریور ۱۴۰۰
  • کاربر مهمان

    مطلب بسیار جالبی هست.با تشکر

    ارسال شده در تاریخ شنبه، ۲۰ شهریور ۱۴۰۰
  • کاربر مهمان

    نکته جالبی بود، در ضمن ممنون از معرفی کتابخانه Benchmark

    ارسال شده در تاریخ شنبه، ۲۰ شهریور ۱۴۰۰
  • کاربر مهمان

    ممنونم از توضیحات خوبتون.می تونیم از کلاس LinkedList یا همون لیست های پیوندی استفاده کنیم. در این صورت هم از حافظه به صورت بهینه استفاده کردیم و هم با محدودیت های آرایه روبرو نمی شیم.https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.linkedlist-1?view=net-5.0

    ارسال شده در تاریخ یکشنبه، ۲۱ شهریور ۱۴۰۰