تدارک یک آزمایش برای بررسی میزان افزایش کارآیی متدهای LINQ در دات نت 7
در ادامه یک آزمایش سادهی بررسی کارآیی متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum را با استفاده از کتابخانهی معروف BenchmarkDotNet مشاهده میکنید:
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using System.Collections.Generic; using System.Linq; [MemoryDiagnoser(displayGenColumns: false)] public partial class Program { static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); [Params (10, 10000)] public int Size { get; set; } private IEnumerable<int> items; [GlobalSetup] public void Setup() { items = Enumerable.Range(1, Size).ToArray(); } [Benchmark] public int Min() => items.Min(); [Benchmark] public int Max() => items.Max(); [Benchmark] public double Average() => items.Average(); [Benchmark] public int Sum() => items.Sum(); }
در مورد کار با آرایهها:
- زمان اجرای یافتن Min در آرایههای کوچک، در دات نت 7، نسبت به دات نت 6، حدودا 10 برابر کاهش یافته و اگر این آرایه بزرگتر شود و برای مثال حاوی 10 هزار المان باشد، این زمان 20 برابر کاهش یافتهاست.
- این کاهش زمانها برای سایر متدهای LINQ نیز تقریبا به همین صورت است؛ منها متد Sum که اندازهی آرایه، تاثیری را بر روی نتیجهی نهایی ندارد.
- همچنین در دات نت 7، با فراخوانی متدهای LINQ، افزایش حافظهای مشاهده نمیشود.
در مورد کار با لیستها:
- در دات نت 6، اعمال صورت گرفتهی توسط LINQ بر روی آرایهها، نسبت به لیستها، همواره سریعتر است.
- در دات نت 7 هم در مورد مجموعههای کوچک، وضعیت همانند دات نت 6 است. اما اگر مجموعهها بزرگتر شوند، تفاوتی بین مجموعهها و آرایهها وجود ندارد و حتی وضعیت مجموعهها بهتر است: کارآیی کار با لیستها 32 برابر بیشتر شدهاست!
اما چگونه در دات نت 7، چنین بهبود کارآیی خیرهکنندهای در متدهای LINQ حاصل شدهاست؟
برای بررسی چگونگی بهبود کارآیی متدهای LINQ در دات نت 7 باید به نحوهی پیاده سازی آنها در نگارشهای مختلف دات نت مراجعه کرد. برای مثال پیاده سازی متد الحاقی Min تا دات نت 6 به صورت زیر است:
public static int Min(this IEnumerable<int> source) { if (source == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); } int value; using (IEnumerator<int> e = source.GetEnumerator()) { if (!e.MoveNext()) { ThrowHelper.ThrowNoElementsException(); } value = e.Current; while (e.MoveNext()) { int x = e.Current; if (x < value) { value = x; } } } return value; }
اما ... پیاده سازی این متد در دات نت 7 متفاوت است:
public static int Min(this IEnumerable<int> source) => MinInteger(source); private static T MinInteger<T>(this IEnumerable<T> source) where T : struct, IBinaryInteger<T> { T value; if (source.TryGetSpan(out ReadOnlySpan<T> span)) { if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2) { .... // Optimized implementation return ....; } } .... //Implementation as in .NET 6 }
اما ... ReadOnlySpan چیست؟ نوعهای Span و ReadOnlySpan، یک ناحیهی پیوستهی مدیریت شده و مدیریت نشدهی حافظه را بیان میکنند. یک Span از نوع ref struct است؛ یعنی تنها میتواند بر روی stack قرار گیرد که مزیت آن، عدم نیاز به تخصیص حافظهی اضافی و بهبود کارآیی است. همچنین ساختار داخلی Span در سی شارپ 11 اندکی تغییر کردهاست که در آن از ref fields جهت دسترسی امن به این ناحیهی از حافظه استفاده میشود. پیشتر از نوع داخلی ByReference برای اشاره به ابتدای این ناحیهی از حافظه استفاده میشد که به همراه بررسی امنیتی در این باره نبود.
پس از دریافت ReadOnlySpan، به سطر زیر میرسیم:
if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2)
private static T MinInteger<T>(this IEnumerable<T> source) where T : struct, IBinaryInteger<T> { .... if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2) { var mins = new Vector<T>(span); index = Vector<T>.Count; do { mins = Vector.Min(mins, new Vector<T>(span.Slice(index))); index += Vector<T>.Count; } while (index + Vector<T>.Count <= span.Length); value = mins[0]; for (int i = 1; i < Vector<T>.Count; i++) { if (mins[i] < value) { value = mins[i]; } } .... }