是什么讓.NET7的Min和Max方法性能暴增了45倍?

簡介在之前的一篇文章.NET性能系列文章一:.NET7的性能改進中我們聊到Linq中的Min()和Max()方法.NET7比.NET6有高達45倍的性能提升,當時Benchmark代碼和結果如下所示:
[Params(1000)]public int Length { get; set; }private int[] arr;[GlobalSetup]public void GlobalSetup() => arr = Enumerable.Range(0, Length).ToArray();[Benchmark]public int Min() => arr.Min();[Benchmark]public int Max() => arr.Max();方法運行時數組長度平均值比率分配Min

是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
10003,494.08 ns53.2432 BMin
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
100065.64 ns1.00-Max
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
10003,025.41 ns45.9232 BMax
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
100065.93 ns1.00-
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
可以看到有高達45倍的性能提升,那就有小伙伴比較疑惑,在.NET7中到底是做了什么讓它有如此大的性能提升?所以本文就通過.NET7中的一些pr帶大家一起探索下.NET7的Min()和Max()方法是如何變快的 。
探索首先我們打開.NET Runtime的倉庫,應該沒有人不會知道倉庫的地址吧?里面包含了.NET運行時所有的代碼,包括CLR和BCL庫 。地址如下所示:https://github.com/dotnet/runtime
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
然后我們熟練的根據命名空間System.Linq找到Linq所在的文件夾位置 , 如下所示:
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
可以看到很多Linq相關的方法都在這個文件夾內,讓我們先來找一找Max()方法所對應的類 。就是下方所示,我們可以看到剛好異步小王子Stephen Toub大佬提交了一個優化代碼 。
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
然后我們點擊History查看這個類的提交歷史,我們發現Stephen大佬在今年多次提交代碼 , 都是優化其性能 。
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
找到Stephen大佬的第一個提交,我們發現在Max的代碼中,多了一個特殊的路徑,如果數據類型為int[] , 那么就走單獨的一個方法重載,并在這個重載中啟用了SIMD向量化,代碼如下所示:
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
SIMD向量化在我之前的多篇文章中都有提到(如:.NET如何快速比較兩個byte數組是否相等),它是CPU的特殊指令 , 使用它可以大幅度的增強運算性能,我猜這就是性能提升的原因 。
我們可以看到在上面只為int[]做了優化 , 然后繼續瀏覽了Stephen大佬的其它幾個PR , Stephen大佬將代碼抽象了一下 , 使用了泛型的特性,然后順便為其它的基本值類型都做了優化 。能享受到性能提升的有byte sbyte ushort short uint int ulong long nuint nint 。
是什么讓.NET7的Min和Max方法性能暴增了45倍?

文章插圖
所以我們以最后一個提交為例 , 看看到底是用了什么SIMD指令,什么樣的方法來提升的性能 。抽取出來的核心代碼如下所示:
private static T MinMaxInteger<T, TMinMax>(this IEnumerable<T> source)    where T : struct, IBinaryInteger<T>    where TMinMax : IMinMaxCalc<T>{    T value;    if (source.TryGetSpan(out ReadOnlySpan<T> span))    {        if (span.IsEmpty)        {            ThrowHelper.ThrowNoElementsException();        }        // 判斷當前平臺是否支持使用Vector-128 或者 總數據長度是否小于128位        // Vector128是指硬件支持同時計算128位二進制數據        if (!Vector128.IsHardwareAccelerated || span.Length < Vector128<T>.Count)        {            // 進入到此路徑,說明最基礎的Vector128都不支持,那么直接使用for循環來比較            value = span[0];            for (int i = 1; i < span.Length; i++)            {                if (TMinMax.Compare(span[i], value))                {                    value = span[i];                }            }        }        // 判斷當前平臺是否支持使用Vector-256 或者 總數據長度是否小于256位        // Vector256是指硬件支持同時計算256位二進制數據        else if (!Vector256.IsHardwareAccelerated || span.Length < Vector256<T>.Count)        {            // 進入到此路徑,說明支持Vector128但不支持Vector256            // 那么進入128位的向量化的比較            // 獲取當前數組的首地址,也就是指向第0個元素            ref T current = ref MemoryMarshal.GetReference(span);            // 獲取Vector128能使用的最后地址,因為整個數組占用的bit位有可能不能被128整除            // 也就是說最后的尾巴不夠128位讓CPU跑一次,那么就直接最后往前數128位 , 讓CPU能完整的跑完            ref T lastVectorStart = ref Unsafe.Add(ref current, span.Length - Vector128<T>.Count);            // 從內存首地址加載0-127bit數據,作為最大值的基準            Vector128<T> best = Vector128.LoadUnsafe(ref current);            // 計算下一個的位置,也就是偏移128位            current = ref Unsafe.Add(ref current, Vector128<T>.Count);            // 循環比較 確保地址小于最后地址            while (Unsafe.IsAddressLessThan(ref current, ref lastVectorStart))            {                // 此時TMinMax.Compare重載代碼 => Vector128.Max(left, right);                // Vector128.Max 會根據類型一一比較 , 每x位最大的返回,                // 比如int就是每32位比較,詳情可以看我后文的解析                best = TMinMax.Compare(best, Vector128.LoadUnsafe(ref current));                current = ref Unsafe.Add(ref current, Vector128<T>.Count);            }            // 最后一組Vector128進行比較            best = TMinMax.Compare(best, Vector128.LoadUnsafe(ref lastVectorStart));            // 由于Vector128最后的結果是128位,比如我們類型是int32,那么最后的結果就有            // 4個int32元素,我們還需要從這4個int32元素中找到最大的            value = best[0];            for (int i = 1; i < Vector128<T>.Count; i++)            {                // 這里 TMinMax.Compare就是簡單的大小于比較                // left > right                if (TMinMax.Compare(best[i], value))                {                    value = best[i];                }            }        }        else        {            // Vector256執行流程和Vector128一致            // 只是它能一次性判斷256位,舉個例子就是一個指令8個int32            ref T current = ref MemoryMarshal.GetReference(span);            ref T lastVectorStart = ref Unsafe.Add(ref current, span.Length - Vector256<T>.Count);            Vector256<T> best = Vector256.LoadUnsafe(ref current);            current = ref Unsafe.Add(ref current, Vector256<T>.Count);            while (Unsafe.IsAddressLessThan(ref current, ref lastVectorStart))            {                best = TMinMax.Compare(best, Vector256.LoadUnsafe(ref current));                current = ref Unsafe.Add(ref current, Vector256<T>.Count);            }            best = TMinMax.Compare(best, Vector256.LoadUnsafe(ref lastVectorStart));            value = best[0];            for (int i = 1; i < Vector256<T>.Count; i++)            {                if (TMinMax.Compare(best[i], value))                {                    value = best[i];                }            }        }    }    else    {        // 如果不是基本類型的數組,那么進入迭代器,使用原始方法比較        using (IEnumerator<T> e = source.GetEnumerator())        {            if (!e.MoveNext())            {                ThrowHelper.ThrowNoElementsException();            }            value = e.Current;            while (e.MoveNext())            {                T x = e.Current;                if (TMinMax.Compare(x, value))                {                    value = x;                }            }        }    }    return value;}

推薦閱讀