我们可以看到在上面只为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
中保存的是x
和y
对应位置的最大值,这样是不是就觉得清晰明了,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又是如何提升其它方面的性能的。
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!