عنوان:

‫معرفی ZLinq: کتابخانه LINQ با تخصیص حافظه صفر برای دات‌نت


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۰۱ ۰۷:۵۰
آدرس: www.dntips.ir
کتابخانه ZLinq، که به تازگی نسخه ۱ آن منتشر شده، یک پیاده‌سازی نوین از LINQ برای دات‌نت است که هدف اصلی آن، دستیابی به «تخصیص حافظه صفر» (Zero-Allocation) است. این کتابخانه با بهره‌گیری از ساختارها (structs) و انواع عمومی (generics)، جایگزینی قدرتمند و بهینه‌تر برای LINQ استاندارد دات‌نت ارائه می‌دهد. ZLinq توانسته است بیش از ۲۰۰۰ ستاره در گیت‌هاب کسب کند و نشان‌دهنده علاقه جامعه توسعه‌دهندگان به این رویکرد جدید است. این پروژه با وجود سابقه تلاش‌های متعدد برای پیاده‌سازی LINQ مبتنی بر ساختار، به دلیل جامعیت و بهینه‌سازی‌های پیشرفته، از سایر تلاش‌ها متمایز می‌شود.

مزایای اصلی و اهداف ZLinq
ZLinq با هدف حل مشکل تخصیص حافظه در LINQ استاندارد توسعه یافته است. همانطور که در بنچمارک‌های ارائه شده مشخص است، تخصیص حافظه در LINQ استاندارد با افزایش تعداد متدهای متوالی (chaining) افزایش می‌یابد، در حالی که ZLinq تخصیص حافظه را در حد صفر نگه می‌دارد. این کتابخانه ۱۰۰ درصد متدها و سربارگذاری‌های (overloads) موجود در .NET 10 را پوشش می‌دهد، از جمله موارد جدیدی مانند Shuffle، RightJoin و LeftJoin. همچنین، ZLinq سازگاری رفتاری ۹۹ درصدی را با LINQ استاندارد تضمین می‌کند که این امر با پورت کردن و اجرای مداوم تست‌های System.Linq.Tests از dotnet/runtime به دست آمده است. علاوه بر کاهش تخصیص حافظه، ZLinq شامل بهینه‌سازی‌های پیشرفته‌ای از جمله پشتیبانی از SIMD است تا عملکرد بهتری را در اکثر سناریوهای عملی ارائه دهد. یکی از مزایای مهم ZLinq این است که این بهینه‌سازی‌ها را برای تمام نسل‌های .NET، از جمله .NET Framework، فراهم می‌کند، نه فقط برای نسخه‌های اخیر.

معماری و بهینه‌سازی‌ها
هسته ZLinq بر پایه ValueEnumerable و IValueEnumerator بنا شده است. ValueEnumerable یک readonly ref struct است که IValueEnumerator را کپسوله می‌کند. این طراحی امکان پردازش با وضعیت مستقل را برای هر Enumerator فراهم می‌آورد. این رویکرد به جای استفاده از IValueEnumerable انتخاب شده است تا از محدودیت‌های استنتاج نوع (type inference) در C# جلوگیری شود.
یکی از نوآوری‌های اصلی در IValueEnumerator، متد TryGetNext(out T current) است که جایگزین جفت MoveNext() و Current در رابط IEnumerator شده است. این تغییر، تعداد فراخوانی متد را در هر تکرار به یک مورد کاهش داده و نیاز به نگهداری فیلد current را از بین می‌برد. این امر به طور قابل توجهی اندازه ساختار (struct size) را کاهش می‌دهد که برای معماری مبتنی بر ساختار ZLinq حیاتی است، زیرا اندازه کوچکتر ساختارها منجر به هزینه کپی کمتر و عملکرد بهتر می‌شود. اگرچه این رویکرد از هم‌وردایی (covariance) و پادهم‌وردایی (contravariance) پشتیبانی نمی‌کند، اما توسعه‌دهنده ZLinq معتقد است که این ویژگی‌ها با Span سازگار نیستند و در .NET مدرن منسوخ شده‌اند.
IValueEnumerator همچنین شامل متدهای کمکی برای بهینه‌سازی است:
  • TryGetNonEnumeratedCount(out int count): این متد زمانی موفق می‌شود که منبع اصلی دارای تعداد متناهی باشد و متدهای فیلترینگ (مانند Where و Distinct) در زنجیره عملیات وجود نداشته باشند. این بهینه‌سازی برای متدهایی مانند ToArray و متدهایی که به بافرهای میانی نیاز دارند (مانند OrderBy و Shuffle) مفید است.
  • TryGetSpan(out ReadOnlySpan: این متد امکان دسترسی به داده‌ها را به عنوان حافظه پیوسته فراهم می‌کند، که می‌تواند منجر به بهبود چشمگیر عملکرد از طریق عملیات SIMD یا پردازش حلقه مبتنی بر Span شود.
  • TryCopyTo(scoped Span: این متد عملکرد را از طریق تکرارکننده‌های داخلی (internal iterators) افزایش می‌دهد. برخلاف CopyTo استاندارد .NET، این متد شامل Index offset است و به destination اجازه می‌دهد که از منبع کوچکتر باشد، که امکان پوشش موارد بهینه‌سازی بیشتری را با یک متد واحد فراهم می‌کند، به عنوان مثال برای نمایش ElementAt، First و Last.

LINQ to Span و LINQ to SIMD
ZLinq از .NET 9 و نسخه‌های بالاتر، امکان زنجیره‌سازی تمام عملگرهای LINQ را روی Span و ReadOnlySpan فراهم می‌کند. این قابلیت با معرفی ویژگی allows ref struct در C# 13 و .NET 9 ممکن شده است، که به ZLinq اجازه می‌دهد Span و IEnumerable را به طور یکسان پردازش کند.
ZLinq همچنین از SIMD برای سرعت بخشیدن به متدهای تجمیعی خاصی مانند Sum، Max، Min، Contains و SequenceEqual استفاده می‌کند. این بهینه‌سازی با بهره‌گیری از IValueEnumerator.TryGetSpan به مجموعه‌هایی که می‌توانند به عنوان Span دسترسی پیدا کنند، اعمال می‌شود. ZLinq متد SumUnchecked را نیز معرفی کرده است که سریع‌تر از Sum استاندارد است زیرا بررسی‌های سرریز (overflow checks) را انجام نمی‌دهد. برای عملیات‌هایی که به صراحت از SIMD بهره می‌برند، متد AsVectorizable() ارائه شده است که به توسعه‌دهندگان امکان می‌دهد عملیات‌های قابل اعمال SIMD را فراخوانی کنند. همچنین، VectorizedUpdate برای آرایه‌ها و Spanها، پردازش موازی عناصر را با استفاده از SIMD فراهم می‌کند.

LINQ to Tree
با الهام از LINQ to XML، ZLinq مفهوم LINQ to Tree را گسترش داده است. این ویژگی امکان عملیات‌های LINQ-مانند را بر روی ساختارهای درختی فراهم می‌کند. ZLinq یک رابط ITraverser تعریف کرده است که به صورت عمومی امکان دسترسی به Ancestors، Children، Descendants، BeforeSelf و AfterSelf را فراهم می‌آورد. ZLinq شامل پیاده‌سازی‌های استاندارد برای FileSystem (درخت دایرکتوری)، JSON (برای JsonNode از System.Text.JsonGameObject و Transform در Unity و Node در Godot است. این پیاده‌سازی‌ها به دقت برای عملکرد و تخصیص حافظه بهینه شده‌اند.


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