我正在编写一些性能敏感的代码,其中无符号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);
}
我想使用内在函数使其更快。我很清楚如何使用BMI2(比便携式版本快50%):
ulong lo;
ulong hi = System.Runtime.Intrinsics.X86.Bmi2.X64.MultiplyNoFlags(x, y, &lo);
return (hi, lo);
我还不清楚如何使用其他可用的内在函数。它们似乎都依赖于Vector<128>
类型,而它们似乎都没有处理ulong
类型。
如何使用SSE,AVX等实现ulong
的乘法?
答案 0 :(得分:2)
SIMD向量不是单个宽整数。最大元素宽度为64位。它们用于并行处理多个元素。
x86没有针对64x64 => 128位SIMD元素乘法的指令,甚至对于AVX512DQ也没有。(尽管确实提供了SIMD 64x64 => 64位乘法,但2 ,4或8个元素并行。)
AVX512IFMA(在Cascade Lake中)具有52位的high and low-half multiply-accumulate(这不是double
的有效宽度的巧合; SIMD整数乘法指令使用与FP相同的乘法硬件)。
因此,如果您想要64x64 => 128位SIMD乘法,则必须将其从4x 32x32 => 64位vpmuludq
中合成出来,并进行一些加法,包括一个d必须再次从多个指令进行合成。
即使有AVX512,对于数组乘法,这可能比标量mul r64
慢。只需4个标量mul
指令即可产生512位乘法结果,现代x86 CPU完全流水线mul
使它们每个时钟可以产生1对结果。 (当然,直到IceLake / Sunny Cove为止,存储吞吐量只有每个时钟1个,因此要获得存储的64位结果的两半都是一个问题!但是将数据移至128位存储的XMM寄存器会花费更多的时间,而且还会每秒64位的瓶颈。)
如果仅需要64x64 => 64位乘法,则可以删除high32*high32
乘法。我在Fastest way to multiply an array of int64_t?中编写了C ++版本,它的 几乎比使用AVX2在Haswell上的标量要快,但是在Skylake上要快得多。无论哪种方式,没有AVX2都不值得。
BTW,您不需要BMI2即可进行标量64x64 => 128位乘法。
这是x86-64的基准,使用单操作数mul
(无符号)或imul
(有符号)。如果C#公开了BMI2 mulx
的内在函数,那么它肯定必须公开一个普通的无符号mul
和带符号的imul
,这在大多数情况下(和较小的代码大小)至少是一样有效的。>