令我惊讶的是,与 Vector256 不同,Vector64 中的 ExtractMostSignificantBits 函数并没有使用内部函数来完成工作。使用内部函数后,以下 36 行代码如下:
mov qword ptr [rsp+0x80], r12
movzx rdx, byte ptr [rsp+0x80]
shr edx, 7
movzx r8, byte ptr [rsp+0x81]
shr r8d, 7
add r8d, r8d
or edx, r8d
movzx r8, byte ptr [rsp+0x82]
shr r8d, 7
shl r8d, 2
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x83]
shr r8d, 7
shl r8d, 3
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x84]
shr r8d, 7
shl r8d, 4
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x85]
shr r8d, 7
shl r8d, 5
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x86]
shr r8d, 7
shl r8d, 6
or r8d, edx
mov edx, r8d
movzx r8, byte ptr [rsp+0x87]
shr r8d, 7
shl r8d, 7
or r8d, edx
分为这4个:
mov qword ptr [rsp+0x70], rcx
mov r10, qword ptr [rsp+0x70]
mov r15, 0x8080808080808080
pext r10, r10, r15
似乎你可以用这样的方法将它卷入Vector64.cs :
public static uint ExtractMostSignificantBits<T>(this Vector64<T> vector)
{
if (Bmi2.X64.IsSupported)
{
ulong mask = 0;
if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte))
mask = 0x8080808080808080;
else if (typeof(T) == typeof(short) || typeof(T) == typeof(ushort))
mask = 0x8000800080008000;
else if (typeof(T) == typeof(int) || typeof(T) == typeof(uint) || typeof(T) == typeof(float))
mask = 0x8000000080000000;
else if (typeof(T) == typeof(long) || typeof(T) == typeof(ulong) || typeof(T) == typeof(double))
mask = 0x8000000000000000;
else if (typeof(T) == typeof(nint) || typeof(T) == typeof(nuint))
if (IntPtr.Size == 4)
mask = 0x8000000080000000;
else
mask = 0x8000000000000000;
ulong u = vector._00;
ulong retval = Bmi2.X64.ParallelBitExtract(u, mask);
return (uint)retval;
}
// Fall back to the old code
uint result = 0;
for (int index = 0; index < Vector64<T>.Count; index++)
{
uint value = Scalar<T>.ExtractMostSignificantBit(vector.GetElementUnsafe(index));
result |= (value << index);
}
return result;
}
BMI2 已经不算“新”了,早在 2013 年左右就已推出。而且,考虑到 JIT 编译,这样做成本也不高。
我已经编写了一些测试代码(可根据要求提供),它产生的结果与现有代码相同,只是速度快了两倍多(如果对 Vector64.GetElementUnsafe 进行一些改进,速度可能会更快)。
我是不是忽略了什么细微差别或特殊情况,导致使用内在函数不可接受?还是有人只是忽略了这一点?
所以,总结一下。我提出的解决方案不是.Net团队在撰写时选择的解决方案,原因(至少)有3个
Vector64.ExtractMostSignificantBits
:BMI2 系列在 Excavator 和 Zen1/Zen2 系列处理器上的性能表现极其糟糕。虽然添加我建议的代码可以提升我机器的性能,但应尽可能避免让其他人的体验更差。
BMI2 系列比 SSE 系列更新,这意味着使用 SSE 指令不仅可以避免 Zen1/Zen2 问题,还能支持更老的机器。BMI2 于 2013 年推出,而 SSE2 则可追溯到 2000 年。
英特尔的低端(赛扬/奔腾)CPU 和低功耗(Silvermont 系列)直到后来才分别在 Ice Lake Pentium/Celeron 和 Gracemont(Alder Lake E 核)上获得 BMI2 支持。奔腾/赛扬品牌的 Skylake 衍生 CPU 及更早的 CPU 禁用了 AVX1/2,BMI1/2 也禁用了,可能是通过禁用 VEX 前缀解码来实现的。
即使在处理更多位时,SSE 指令也能比 BMI2 指令生成更快的代码。
看看这些数字:
因此,正如大家预测的那样,使用 128 位矢量指令显然是赢家。
现在我将利用我所学到的知识并将其推销给 .Net 人员,看看他们是否感兴趣。