
在之前的一篇文章.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();方法运行时数组长度平均值比率分配Min10003,494.08 ns53.2432 BMin100065.64 ns1.00-Max10003,025.41 ns45.9232 BMax100065.93 ns1.00-可以看到有高达45倍的性能提升那就有小伙伴比较疑惑在.NET7中到底是做了什么让它有如此大的性能提升所以本文就通过.NET7中的一些pr带大家一起探索下.NET7的Min()和Max()方法是如何变快的。探索#首先我们打开.NET Runtime的仓库应该没有人不会知道仓库的地址吧里面包含了.NET运行时所有的代码包括CLR和BCL库。地址如下所示https://github.com/dotnet/runtime然后我们熟练的根据命名空间System.Linq找到Linq所在的文件夹位置如下所示可以看到很多Linq相关的方法都在这个文件夹内让我们先来找一找Max()方法所对应的类。就是下方所示我们可以看到刚好异步小王子Stephen Toub大佬提交了一个优化代码。然后我们点击History查看这个类的提交历史我们发现Stephen大佬在今年多次提交代码都是优化其性能。找到Stephen大佬的第一个提交我们发现在Max的代码中多了一个特殊的路径如果数据类型为int[]那么就走单独的一个方法重载并在这个重载中启用了SIMD向量化代码如下所示SIMD向量化在我之前的多篇文章中都有提到(如.NET如何快速比较两个byte数组是否相等)它是CPU的特殊指令使用它可以大幅度的增强运算性能我猜这就是性能提升的原因。我们可以看到在上面只为int[]做了优化然后继续浏览了Stephen大佬的其它几个PRStephen大佬将代码抽象了一下使用了泛型的特性然后顺便为其它的基本值类型都做了优化。能享受到性能提升的有byte sbyte ushort short uint int ulong long nuint nint。所以我们以最后一个提交为例看看到底是用了什么SIMD指令什么样的方法来提升的性能。抽取出来的核心代码如下所示private static T MinMaxIntegerT, TMinMax(this IEnumerableT source) where T : struct, IBinaryIntegerT where TMinMax : IMinMaxCalcT { T value; if (source.TryGetSpan(out ReadOnlySpanT span)) { if (span.IsEmpty) { ThrowHelper.ThrowNoElementsException(); } // 判断当前平台是否支持使用Vector-128 或者 总数据长度是否小于128位 // Vector128是指硬件支持同时计算128位二进制数据 if (!Vector128.IsHardwareAccelerated || span.Length Vector128T.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 Vector256T.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, s