بیاین کمی راجب Memory Allocation حرف بزنیم!
میدونیم توی سی شارپ، String ها نوع خاصی از داده هستن. با اینکه Reference Type هستن ولی مثل Value Type رفتار میکنن. بخاطر کار زیاد باهاشون و خاص بودنشون، توی Heap یه قسمت خاص براشون در نظر گرفته شده. String ها پشت صحنه یجورایی یه آرایه از کاراکتر هستن، به همین خاطر چون تغییر دادنشون عملیات سنگینیه، Immutable ان و اگر تغییرش بدین، یک String جدید ساخته میشه.
تا اینجای مطلب رو به احتمال زیاد میدونستین یا راجبش شنیده بودین.
شاید تا حالا اسم تایپ
Span یا
Memory و امثالشون رو دیده باشین. بخصوص توی نسخه های اخیر دات نت توی Standard Library ها بیشتر دیده میشن.
اگر ما بخوایم یه بخشی از یه String رو بخونیم، باید slice یا split اش بکنیم و ازونجایی که گفتیم نمیشه به String دست زد، یعنی هرباری که اینکاری میکنیم داریم String جدید ایجاد میکنیم (Memory Allocation). و یعنی اگر یه لیستی از String ها داشته باشیم و بخوایم روی هر کدوم عملیات تغییر کننده انجام بدیم، به تعداد کل لیست، حافظه جدید اشغال میشه. توی تعداد کم، مثلا تا 100 هزارتا و یا شاید 1 میلیون تا، بسته به سیستم، شاید بشه چشم پوشی کرد. اما اگه تعداد بالا بره، حافظه پر میشه و ممکنه برنامه crash کنه.
خب چطوری میتونیم Memory Allocation رو پایین بیاریم؟ باید به یه روشی بتونیم یه بخشی از یه String رو بخونیم بدون اینکه بخوایم تغییرش بدیم، ازونجایی که String پشت صحنه یه آرایه ست، با استفاده از Span میتونیم بصورت مستقیم به بخشی ازون آرایه دسترسی داشته باشیم و دیگه حافظه جدید اشغال نکنیم!
حالا واقعا خود Span و Memory چی هستن؟ به ما دسترسی مستقیم به حافظه هر لیستی میدن بدون اینکه حافظه بیشتری اشغال بشه، و با توجه به ماهیت String میشه روی اون هم ازشون استفاده کرد.
توی این مقاله از خود مایکروسافت میتونین بهتر باهاشون آشنا بشین:
https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelinesاما میخوام بهتون تفاوت پرفورمنس رو نشون بدم.
من یه لیست از 10 میلیون String توی یه فایل دارم و بعد از خوندنش با استفاده از LINQ میام هر آیتم رو split میکنم، قسمت اول و دوم رو جدا میکنم و توی یه آبجکت ذخیره میکنم. روی سیستم من این نتیجه رو داد:
Performance:
Rows: 10,000,000
Stations: 413
Elapsed: 00:00:06.4921942
Throughput: 1,540,311 rows/sec (36.72 MB/sec)
Memory:
Working Set: 1,582 MB
GC Memory: 1,357 MB
Gen0 Collections: 169
Gen1 Collections: 169
Gen2 Collections: 10
حالا همین تست رو میام با روشی مینویسم که Memory Allocation کمتر داشته باشه، و از طرفی از Span هم استفاده بکنه و نتیجه شد این:
Performance:
Rows: 10,000,000
Stations: 413
Elapsed: 00:00:01.8735861
Throughput: 5,337,358 rows/sec (127.25 MB/sec)
Memory:
Working Set: 778 MB
GC Memory: 750 MB
Gen0 Collections: 47
Gen1 Collections: 47
Gen2 Collections: 8
البته باید بگم که برای این پست نیومدم روش کامل بهینه سازی رو انجام بدم. فقط اون قسمت مربوط به Memory Allocation توی لوپ اصلی رو بهتر کردم و توی هردو تست کل فایل رو یک جا خوندم و پردازش کردم. که این خودش یه عالمه جابجایی انجام میده. توی مرحله بعدی، حتی قسمت خوندن از فایل هم بهینه کردم و نتیجه شگفت انگیز شد:
Performance:
Rows: 10,000,000
Stations: 413
Elapsed: 00:00:01.1981524
Throughput: 8,346,184 rows/sec (198.99 MB/sec)
Memory:
Working Set: 54 MB
GC Memory: 4 MB
Gen0 Collections: 42
Gen1 Collections: 2
Gen2 Collections: 2
یعنی برای 10 میلیون خط فقط 54 مگابایت توی حافظه نگه میداره! درحالی که توی تست قبلی حدودا 1 و نیم گیگابایت درگیر بود!
اینجا اومدم فایل رو Stream کردم بجای اینکه کامل بخونم که باعث میشه فقط اون بخشی از فایل که میخوام رو بیاره توی حافظه و وقتی کارش تموم شد بده بره.
@ever_code