使用.NET Core的硬件内在函数乘以64位整数

时间:2019-05-07 09:23:24

标签: c# math .net-core intrinsics .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);
}

我想使用内在函数使其更快。我很清楚如何使用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的乘法?

1 个答案:

答案 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,这在大多数情况下(和较小的代码大小)至少是一样有效的。