简介在之前的一篇文章.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
文章插图
10003,494.08 ns53.2432 BMin
文章插图
100065.64 ns1.00-Max
文章插图
10003,025.41 ns45.9232 BMax
文章插图
100065.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大佬的其它几个PR , Stephen大佬将代码抽象了一下 , 使用了泛型的特性,然后顺便为其它的基本值类型都做了优化 。能享受到性能提升的有byte sbyte ushort short uint int ulong long nuint nint 。
文章插图
所以我们以最后一个提交为例 , 看看到底是用了什么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;}
推荐阅读
- React动画实现方案之 Framer Motion,让你的页面“自己”动起来
- 暗黑破坏神:不朽遗志获取方法是什么
- 原神破绽的捕捉方法是什么
- 迷你世界9月14日礼包兑换方式是什么
- 白苹果是什么意思,白苹果怎么修复(白苹果会自己恢复吗)
- 原神元能构装体掉落奖励是什么
- 羊了个羊小游戏爆火攻略是什么
- 原神圣显之钥技能是什么
- 原神3.1版本前瞻兑换码是什么
- 卷帙浩繁读音是什么 卷帙浩繁的读音