使用.NET Core的硬件内置函数对64位整数进行乘法运算

7

我正在编写一些对性能要求较高的代码,其中无符号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的乘法?


关于 Vector<ulong> 怎么样?可以使用 System.Numerics.Vectors。但是似乎使用乘法的 ulong 没有任何优势。 - Jeroen van Langen
@J.vanLangen 这有点像我的问题 :) 没有任何内在的乘法函数似乎能够与 ulong 一起使用,例如 Sse2.Multiply - Cocowalla
1个回答

4
SIMD向量不是单个宽度的整数,最大元素宽度为64位。它们用于并行处理多个元素。 x86没有任何用于64x64=>128位SIMD元素乘法的指令,即使是使用AVX512DQ也不行。(但可以用于2、4或8个元素的并行SIMD 64x64=>64位乘法)。 Cascade Lake中的AVX512IFMA具有52位高低半乘加(不是巧合的是double的有效数字宽度),SIMD整数乘法指令使用与FP相同的乘法硬件。
因此,如果您想要64x64 => 128位SIMD乘法,您必须将其合成为4x 32x32 => 64位的vpmuludq和一些加法,包括一个加宽进位,您还必须从多个指令中再次合成。即使使用AVX512,对于一组乘法数组而言,这可能比标量mul r64更慢。只需4个标量mul指令即可生成512位的乘法结果,并且现代x86 CPU完全流水线化了mul,因此它们可以每个时钟周期产生1对结果。(当然,存储吞吐量直到IceLake / Sunny Cove才达到每个时钟周期1个,因此获取64位结果的两个半部分是一个问题!但将数据移动到XMM寄存器以进行128位存储会产生更多的uops,并且还会遇到每个时钟周期64位的瓶颈。)
如果您只需要 64x64 => 64 位乘法,您可以省略 high32*high32 的乘法。我在 Fastest way to multiply an array of int64_t? 中编写了一个 C++ 版本,并且在 Haswell with AVX2 上比标量版本稍微快一些,在 Skylake 上则快得多。无论哪种方式,如果没有 AVX2,它都不值得这样做。

顺便说一下,你不需要BMI2来进行标量64x64 => 128位乘法。

对于x86-64来说,这是基本操作,可以使用单操作数的mul(无符号)或imul(有符号)来实现。如果C#暴露了一个用于BMI2 mulx的内在函数,那么它肯定也必须暴露一个用于普通无符号mul和有符号imul的内在函数,在大多数情况下,这些操作至少同样有效(代码大小更小)。


1
感谢您详细的回复 - 正如您所猜测的那样,我是新手 :) 我确实需要64x64 128位结果的高位和低位。如果mulimul是标准的x86指令集,那么.NET在乘以2个64位整数时已经使用了它们(查看反编译,它使用了一个mul操作码,我猜这是与x86操作码的1:1匹配),但无论如何,.NET没有128位数据类型(尚未),因此这只会给我带来结果的低位。 - Cocowalla
奇怪的是,在.NET Core中没有mulx的内在函数。因此,似乎唯一的方法是编写调用_mul128_umul128的本地代码。或者也可以自己编辑和编译.NET Core - phuclv
2
与128位mul支持相关的各种问题包括:改进x64平台上System.Decimal性能启用长乘法,https://github.com/dotnet/coreclr/pull/21362#discussion_r239273064,https://github.com/dotnet/corefx/issues/32075#issuecomment-420467575 - phuclv
2
@PeterCordes 打错字了。我的意思是在 .NET Core 中没有 mulimul 的内在函数,只有 mulx - phuclv
2
@phuclv:这太蠢了。并非所有的C编译器都有它,但是那些不支持它的主要编译器可以使用内置函数来支持128位整数类型,因此可以从a * (__int128)b中发出。但在GCC出现__int128类型之前,64位平台存在一段时间,所以我猜.NET Core正处于其演变的阶段? - Peter Cordes
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接