首页 > 其他分享 >是什么让.NET7的Min和Max方法性能暴增了45倍?

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

时间:2022-10-28 09:35:23浏览次数:56  
标签:Min Max 45 value current Vector128 ref best

简介

在之前的一篇文章.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又是如何提升其它方面的性能的。

标签:Min,Max,45,value,current,Vector128,ref,best
From: https://www.cnblogs.com/InCerry/p/how_to_use_simd_improve_dotnet7_min_max_performance.html

相关文章

  • AtCoder Beginner Contest 247 E - Max Min
    题目描述简要描述:给定一个长度为\(N\)的数组,求数组的子数组满足最大值为\(X\)且最小值为\(Y\)的子区间的个数。做法1.ST表+二分时间复杂度:\(O(n\logn)\)......
  • new: 轮播图 | MDN上HTML的总结和CSS面试题解答,以及vue-admin/豆瓣一个静态页面的实现
    主要参看oppo官网https://www.oppo.com/cn/,实现以下功能一、轮播图https://www.cnblogs.com/WindrunnerMax/p/12638005.html通常是在首页读秒播放的图片,本次了解的是opp......
  • vue-element-admin框架学习笔记
    0背景vue-element-admin是一个已高度完成的系统原型,它基于vue框架和elementUI组件库。它使用最新的前端技术栈,内置i18n国际化解决方案、动态路由、权限验证。它可以帮助......
  • 安装minikube
    https://minikube.sigs.k8s.io/docs/start/1、下载https://storage.googleapis.com/minikube/releases/latest/minikube-installer.exe2、安装运行cmd-->minikubes......
  • CF1690(Div3) E. Price Maximization 好题
    题目传送门首先,可以发现,我们不关心原数字的大小,只关心他们除以\(k\)之后的余数。如此考虑:两个数相加,\((a+b)/k=a/k+b/k+(a\)\(mod\)\(k+b\)\(mod\)......
  • Ubuntu 安装 PhpMyAdmin 管理 MySQL 数据库
     Ubuntu安装PhpMyAdmin管理MySQL数据库前言:当前服务器有跳板机不能直接用本地navicat连接数据库问题  PhpMyAdmin是一个用PHP编写的软件工具,可以通过......
  • 代码随想录day22 | 235. 二叉搜索树的最近公共祖先 701. 二叉搜索树中的插入操作 45
    235.二叉搜索树的最近公共祖先题目|文章思路在二叉树公共祖先问题中,可以通过后序遍历,从二叉树节点向上遍历,找到最近公共祖先。本题中我们可以利用二叉搜索树的特性对......
  • luogu 4588
    给xx这个数进行操作1m:将 xx 变为x,并输出 x%mod2pos:将 xx 变为 xx 除以第 pos 次操作所乘的数(保证第 pos 次操作一定为类型1,对于每一个类型1的操作至多......
  • LeetCode_Array_64. Minimum Path Sum (C++)
    目录​​1,题目描述​​​​2,思路​​​​3,代码【C++】​​​​4,测试效果​​1,题目描述Givenamxngridfilledwithnon-negativenumbers,findapathfromtopleftt......
  • LeetCode_Array_53. Maximum Subarray (C)
    目录​​1,题目描述​​​​2,思路​​​​基本思路​​​​细节​​​​参考文章​​​​3,代码【C】​​1,题目描述Givenanintegerarraynums,findthecontiguoussubarr......