简介

在之前的一篇文章.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 1000 3,494.08 ns 53.24 32 B
Min 1000 65.64 ns 1.00 -
Max 1000 3,025.41 ns 45.92 32 B
Max 1000 65.93 ns 1.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;
}

以上就是代码的解析,相信很多人疑惑的地方就是Vector128.Max做了什么,我们可以构造一个代码,让大家简单的看出来发生了什么。代码和运行结果如下所示:

// 定义一个数组
var array = new int[] { 4, 3, 2, 1, 1, 2, 3, 4 }; // 拿到数组首地址指针
ref int current = ref MemoryMarshal.GetReference(array.AsSpan()); // 从首地址加载128位数据,上面是int32
// 所以x = 4, 3, 2, 1
var x = Vector128.LoadUnsafe(ref current); // 偏移128位以后,继续加载128位数据
// 所以y = 1, 2, 3, 4
var y = Vector128.LoadUnsafe(ref Unsafe.Add(ref current, Vector128<int>.Count)); // 使用Vector128.Max进行计算
var result = Vector128.Max(x, y); // 打印输出结果
x.Dump();
y.Dump();
result.Dump();



从运行的结果可以看到,result中保存的是xy对应位置的最大值,这样是不是就觉得清晰明了,Stephe大佬上文的代码就是做了这样一个操作。

同样,如果我们把int32换成int64,也就是long类型,由于一个元素占用64位,所以一次只能加载2个int64元素比较最大值,得出对应位置的最大值:

最后使用下面的for循环代码,从result中找到最大的那个int32元素,从我们上文的案例中就是4,结果和代码如下所示:

var value = result[0];
for (int i = 1; i < Vector128<int>.Count; i++)
{
if (value < result[i])
{
value = result[i];
}
}



要注意的是,为了演示方便我这里数组bit长度刚好是128倍数,实际情况中需要考虑不是128倍数的场景。

总结

答案显而易见,试.NET7中Min()Max()方法性能暴增45倍的原因就是Stephe大佬对基本几个连续的值类型比较做了SIMD优化,而这样的优化在本次的.NET7版本中有非常多,后面有时间带大家一起看看SIMD又是如何提升其它方面的性能的。

是什么让.NET7的Min和Max方法性能暴增了45倍?的更多相关文章

  1. 5.7.2.2 min()和max()方法

    Math对象还包含许多方法,用于辅助完成简单和复杂的数学计算. 其中,min()和max()方法用于确定一组数值中的最小值和最大值.这两个方法都可以接受任意多个数值参数,如下例子: var max = ...

  2. Java中Collections的min和max方法

    方法一 public static <T extends Object & Comparable<? super T>> T min(Collection<? e ...

  3. JS单体内置对象之Math常用方法(min,max,ceil,floor,round,random等)

    1.min()和max()方法 Math.min()用于确定一组数值中的最小值.Math.max()用于确定一组数值中的最大值. alert(Math.min(2,4,3,6,3,8,0,1,3)); ...

  4. Linq查询Count、Sum、Min、Max、Average

    原文地址:Linq——Count.Sum.Min.Max.Average作者:mousekitty Linq查询之Count.Sum.Min.Max.Average using System; usi ...

  5. Min and Max

    Min and Max 需要处理不同数据类型; 另外*args, 表示的是位置参数, *kwargs表示的是key参数, args的类型为tuple类型, 参数为min(3, 2)时, args为(3 ...

  6. C语言:min和max头文件

    转自:http://www.cppblog.com/jince/archive/2010/09/14/126600.html min和max头文件 虽然说求最大值最小值函数在哪个头文件下并不是非常重要 ...

  7. 随机获取min和max之间的一个整数

    // 随机获取min和max之间的一个整数 const randomNum = (Min, Max) => { let Range = Max - Min; let Rand = Math.ra ...

  8. mysql中min和max查询优化

    mysql max() 函数的需扫描where条件过滤后的所有行: 在测试环境中重现: 测试版本:Server version:         5.1.58-log MySQL Community ...

  9. min cost max flow算法示例

    问题描述 给定g个group,n个id,n<=g.我们将为每个group分配一个id(各个group的id不同).但是每个group分配id需要付出不同的代价cost,需要求解最优的id分配方案 ...

随机推荐

  1. [数据结构1.2-线性表] 动态数组ArrayList(.NET源码学习)

    [数据结构1.2-线性表] 动态数组ArrayList(.NET源码学习) 在C#中,存在常见的九种集合类型:动态数组ArrayList.列表List.排序列表SortedList.哈希表HashTa ...

  2. Semaphore-停车场

    模拟20辆车进停车场 停车场容纳总停车量5. 当一辆车进入停车场后,显示牌的剩余车位数响应的减1. 每有一辆车驶出停车场后,显示牌的剩余车位数响应的加1. 停车场剩余车位不足时,车辆只能在外面等待 p ...

  3. [CF1539F] Strange Array (线段树)

    题面 有一个长度为 n \tt n n 的序列 a \tt a a ,对于每一个位置 i ∈ [ 1 , n ] \tt i\in[1,n] i∈[1,n]: 选择一个区间 [ l , r ] \tt ...

  4. CF453C Little Pony and Summer Sun Celebration(构造、贪心(?))

    CF453C Little Pony and Summer Sun Celebration 题解 这道题要求输出任意解,并且路径长度不超过4n就行,所以给了我们乱搞构造的机会. 我这里给出一种构造思路 ...

  5. Python自学教程8-数据类型有哪些注意事项

    不知不觉,python自学教程已经更新到第八篇了,再有几篇,基本的语法就介绍完了. 今天来总结一下数据类型有哪些需要注意的地方. 元组注意事项 元组是另一种经常使用到的数据类型,看上去和列表差不多.它 ...

  6. HCNP Routing&Switching之端口安全

    前文我们了解了二层MAC安全相关话题和配置,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/16618201.html:今天我们来聊一聊mac安全的综合解决方案端 ...

  7. HTTP2指纹识别(一种相对不为人知的网络指纹识别方法)

    这是关于网络指纹识别的两部分系列的第二部分 上一部分我介绍了有关TLS 指纹识别方法(以及在不同客户端的指纹有何区别): https://mp.weixin.qq.com/s/BvotXrFXwYvG ...

  8. 部署Zabbix4.0和Grafana

    部署Zabbix4.0和Grafana 一.Zabbix 1.安装 rpm -Uvh https://repo.zabbix.com/zabbix/4.0/rhel/7/x86_64/zabbix-r ...

  9. 快速排序C语言版图文详解

    ​ 算法原理:选一个数位基准,将序列分成两个部分,一边全是比它小序列,另一边全是比它大序列.然后再分别对比他小的序列和比再次进行基准分割.依次分割下去,得到一个有序的队列. 原理图示: ​编辑 ​编辑 ...

  10. jenkins流水线部署springboot应用到k8s集群(k3s+jenkins+gitee+maven+docker)(1)

    前言:前面写过2篇文章,介绍jenkins通过slave节点部署构建并发布应用到虚拟机中,本篇介绍k8s(k3s)环境下,部署jenkins,通过流水线脚本方式构建发布应用到k8s(k3s)集群环境中 ...