Coc*_*lla 5 c# math intrinsics .net-core .net-core-3.0
我正在编写一些性能敏感的代码,其中无符号 64 位整数 ( ulong) 的乘法是一个瓶颈。
.NET Core 3.0 可以使用System.Runtime.Intrinsics命名空间访问硬件内部函数,这太棒了。
我目前正在使用一个可移植的实现,它返回 128 位结果的高位和低位元组:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe (ulong Hi, ulong Lo) Multiply64(ulong x, ulong y)
{
ulong hi;
ulong lo;
lo = x * y;
ulong x0 = (uint)x;
ulong x1 = x >> 32;
ulong y0 = (uint)y;
ulong y1 = y >> 32;
ulong p11 = x1 * y1;
ulong p01 = x0 * y1;
ulong p10 = x1 * y0;
ulong p00 = x0 * y0;
// 64-bit product + two 32-bit values
ulong middle = p10 + (p00 >> 32) + (uint)p01;
// 64-bit product + two 32-bit values
hi = p11 + (middle >> 32) + (p01 >> 32);
return (hi, lo);
}
Run Code Online (Sandbox Code Playgroud)
我想使用内在函数加快速度。我很清楚如何在可用时使用 BMI2(这比便携式版本快约 50%):
ulong lo;
ulong hi = System.Runtime.Intrinsics.X86.Bmi2.X64.MultiplyNoFlags(x, y, &lo);
return (hi, lo);
Run Code Online (Sandbox Code Playgroud)
我完全不清楚如何使用其他可用的内在函数;他们似乎都依赖Vector<128>类型,似乎没有一个人处理ulong类型。
如何ulong使用 SSE、AVX 等实现s 的乘法?
SIMD 向量不是单宽整数。最大元素宽度为 64 位。它们用于并行处理多个元素。
x86 没有任何 64x64 => 128 位 SIMD 元素乘法指令,即使使用 AVX512DQ 也是如此。 (这确实为 2、4 或 8 个并行元素提供了 SIMD 64x64 => 64 位乘法。)
AVX512IFMA(在 Cascade Lake 中)具有 52 位高半和低半乘法累加(这并非巧合,即 的有效数宽度double;SIMD 整数乘法指令使用与 FP 相同的乘法硬件)。
因此,如果您想要 64x64 => 128 位 SIMD 乘法,则必须从 4x 32x32 => 64 位vpmuludq和一些加法中合成它,包括加法宽度进位,您必须再次从多个指令合成它。
mul r64即使 AVX512 可用,这也可能比乘法数组的标量慢。只需 4 条标量mul指令即可产生 512 位乘法结果,而现代 x86 CPU 完全流水线化mul,因此每个时钟可以产生 1 对结果。(当然,在 IceLake / Sunny Cove 之前,存储吞吐量仅为每个时钟 1,因此存储 64 位结果的两半是一个问题!但是,将数据移动到 128 位存储的 XMM 寄存器会花费更多的 uops,并且也会造成每个时钟 64 位瓶颈。)
如果您只需要 64x64 => 64 位乘法,则可以删除high32*high32乘法。我以最快的方式编写了一个 C++ 版本来乘以 int64_t 数组?它在带有 AVX2 的 Haswell 上仅比标量快一点,但在 Skylake 上要快得多。不管怎样,如果没有 AVX2,一切都是不值得的。
顺便说一句,您不需要 BMI2 来执行标量 64x64 => 128 位乘法。
这是 x86-64 的基线,具有单操作数mul(无符号)或imul(有符号)。如果 C# 公开BMI2mulx的内在函数,那么它肯定必须公开普通无符号mul和有符号的内在函数imul,这在大多数情况下至少同样有效(并且代码大小更小)。