计算128位整数模数为64位整数的最快方法

use*_*783 53 c algorithm x86 assembly modulo

我有一个128位无符号整数A和一个64位无符号整数B.什么是最快的计算方法A % B- 即将A除以B的(64位)余数?

我希望用C或汇编语言来做这件事,但我需要针对32位x86平台.遗憾的是,我无法利用编译器对128位整数的支持,也无法利用x64架构在单条指令中执行所需操作的能力.

编辑:

谢谢你到目前为止的答案.但是,在我看来,建议的算法会非常慢 - 执行128位到64位除法的最快方法是利用处理器对64位乘32位除法的原生支持吗?有没有人知道是否有办法在一些较小的部门中执行更大的划分?

回复:B多久换一次?

主要是我对一般解决方案感兴趣 - 如果A和B每次都可能不同,你会进行什么计算?

然而,第二种可能的情况是B不会像A那样经常变化 - 每个B可能有多达200个As除以.在这种情况下,你的答案有何不同?

caf*_*caf 31

您可以使用俄罗斯农民增殖的分部版本.

要查找余数,请执行(伪代码):

X = B;

while (X <= A/2)
{
    X <<= 1;
}

while (A >= B)
{
    if (A >= X)
        A -= X;
    X >>= 1;
}
Run Code Online (Sandbox Code Playgroud)

模数保留在A.

你需要实现移位,比较和减法来操作由一对64位数组成的值,但这是相当微不足道的.

这将循环最多255次(128位A).当然,您需要对零除数进行预检.

  • 代码有bug.有趣的是,它没有在6年内报道.尝试'A = 2,B = 1`进入无限循环.`0x8711dd11 mod 0x4388ee88`失败(结果s/b 1,而不是0x21c47745)以及其他.建议`while(X <A/2)` - >`while(X <= A/2)`来修复.你的伪代码经过测试`unsigned cafMod(unsigned A,unsigned B){assert(B); 无符号X = B; 而(X <A/2){X << = 1; } while(A> = B){if(A> = X)A - = X; X >> = 1; }返回A; } (4认同)
  • @chux:您是绝对正确的,固定的。可能以前没有报告过,因为它仅在A =2ⁿB或A =2ⁿB + 1时发生。谢谢! (2认同)
  • 是的,在 x86 asm 中,将 `x&lt;&lt;=1` 实现为 `add lo,lo`/`adc mid,mid`/... 比 `shl lo`/`rcl mid,1`/... 更高效。但在 C 语言中,编译器应该为你做这件事。当然,在 x86 asm 中,您实际上应该使用 `bsr` (位扫描)或 `lzcnt` (前导零计数)来查找最高设置位的位置,然后使用 `shld hi, mid2, cl` / 。 .. / `shl low, cl` 在一步中完成所有移位,而不是循环第一个 `while (x &lt;= A/2)` 循环。在 32 位模式下,使用 SSE2 进行 64 位元素的 XMM SIMD 移位是很诱人的,特别是为了减少前导零计数 &gt;= 32 的分支 (2认同)

Dal*_*und 13

也许你正在寻找一个完成的程序,但是可以在Knuth的计算机编程艺术第2卷中找到多精度算术的基本算法.你可以在这里找到在线描述的除法算法.算法处理任意多精度算术,因此比您需要的更通用,但您应该能够在64位或32位数字上对128位算术进行简化.准备好合理的工作量(a)理解算法,(b)将其转换为C或汇编程序.

您可能还想查看Hacker's Delight,它充满了非常聪明的汇编程序和其他低级别的hackery,包括一些多精度算法.


MSN*_*MSN 11

鉴于A = AH*2^64 + AL:

A % B == (((AH % B) * (2^64 % B)) + (AL % B)) % B
      == (((AH % B) * ((2^64 - B) % B)) + (AL % B)) % B
Run Code Online (Sandbox Code Playgroud)

如果您的编译器支持64位整数,那么这可能是最简单的方法.MSVC在32位x86上实现64位模数是一些毛茸茸的循环填充程序集(VC\crt\src\intel\llrem.asm对于勇敢的人),所以我个人会这样做.

  • 我不确定我是否理解这是如何工作的.B是64位,因此(AH%B)和((2 ^ 64-B)%B))都是64位.不会将这些放在一起给我们一个128位的数字,因此我们仍然需要执行一个128位到64位的模数? (4认同)
  • @GJ,如果编译器支持64位整数,那么对64位整数使用mod操作会更容易.根据我对程序集的粗略评估,caf的方法是MSVC用于32位x86的方法.它还包括优化低于2 ^ 32的股息.因此,您可以自己编写代码,也可以只使用现有的编译器支持. (2认同)
  • 感谢您的想法,以了解编译器如何在x86上实现64位乘64位模。据我所知,无论是GCC(libgcc2.c中的函数__udivmoddi4)还是MSVC(无符号版本,请参见ullrem.asm)都使用caf的“俄罗斯农民”方法。相反,他们似乎都在Dale Hagglund提供的链接中使用了算法Q的变体(n = 2,b = 32)-使用64位除以32位除法近似于64位除以64位。 ,然后进行微调以校正结果(如有必要)。 (2认同)
  • 这种方法的问题:*乘法需要一个128位的结果,最后一步是some_128_bit_positive_value%some_128_bit_positive_value`,我们回到了开始的地方。尝试0x8000_0000_0000_0000_0000_0000_0000_0000_0000 mod 0xFFFF_FFFF_FFFF_FFFE。我想答案应该是2,但是您的算法给出0(假设乘法的乘积是64位模)。该代码确实适用于“ 128位整数模为32位整数”。也许我的测试是错误的,但是我想知道您的测试结果。 (2认同)
  • @chux:我同意答案应该为0x80000000000000000000000000000000%0xFFFFFFFFFFFFFFFE的2。我在[`cmdline任意精度计算器calc](http://www.isthe.com/chongo/tech/comp/calc/)中进行了测试。我确认将其截断为64位(与(2 ^ 64-1)进行按位与运算)会破坏公式,因此它实际上确实使您处于平方1。`((((AH%B)*(((2 ^ 64- B)%B))&(2 ^ 64-1)+(AL%B))&(2 ^ 64-1)%B == 0`但`((((AH%B)*((2 ^ 64 -B)%B))+(AL%B))%B == 2`。我使用了“ AH = A &gt;&gt; 64”和“ AL = 0”。 (2认同)

GJ.*_*GJ. 8

这几乎是未经测试的部分速度修改的Mod128by64'俄罗斯农民'算法功能.不幸的是我是Delphi用户所以这个功能在Delphi下工作.:)但汇编程序几乎相同所以......

function Mod128by64(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
//   : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//Divisor = edx:ebp
//Dividend = bh:ebx:edx //We need 64 bits + 1 bit in bh
//Result = esi:edi
//ecx = Loop counter and Dividend index
  push    ebx                     //Store registers to stack
  push    esi
  push    edi
  push    ebp
  mov     ebp, [edx]              //Divisor = edx:ebp
  mov     edx, [edx + 4]
  mov     ecx, ebp                //Div by 0 test
  or      ecx, edx                
  jz      @DivByZero
  xor     edi, edi                //Clear result
  xor     esi, esi
//Start of 64 bit division Loop
  mov     ecx, 15                 //Load byte loop shift counter and Dividend index
@SkipShift8Bits:                  //Small Dividend numbers shift optimisation
  cmp     [eax + ecx], ch         //Zero test
  jnz     @EndSkipShiftDividend
  loop    @SkipShift8Bits         //Skip 8 bit loop
@EndSkipShiftDividend:
  test    edx, $FF000000          //Huge Divisor Numbers Shift Optimisation
  jz      @Shift8Bits             //This Divisor is > $00FFFFFF:FFFFFFFF
  mov     ecx, 8                  //Load byte shift counter
  mov     esi, [eax + 12]         //Do fast 56 bit (7 bytes) shift...
  shr     esi, cl                 //esi = $00XXXXXX
  mov     edi, [eax + 9]          //Load for one byte right shifted 32 bit value
@Shift8Bits:
  mov     bl, [eax + ecx]         //Load 8 bits of Dividend
//Here we can unrole partial loop 8 bit division to increase execution speed...
  mov     ch, 8                   //Set partial byte counter value
@Do65BitsShift:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  setc    bh                      //Save 65th bit
  sub     edi, ebp                //Compare dividend and  divisor
  sbb     esi, edx                //Subtract the divisor
  sbb     bh, 0                   //Use 65th bit in bh
  jnc     @NoCarryAtCmp           //Test...
  add     edi, ebp                //Return privius dividend state
  adc     esi, edx
@NoCarryAtCmp:
  dec     ch                      //Decrement counter
  jnz     @Do65BitsShift
//End of 8 bit (byte) partial division loop
  dec     cl                      //Decrement byte loop shift counter
  jns     @Shift8Bits             //Last jump at cl = 0!!!
//End of 64 bit division loop
  mov     eax, edi                //Load result to eax:edx
  mov     edx, esi
@RestoreRegisters:
  pop     ebp                     //Restore Registers
  pop     edi
  pop     esi
  pop     ebx
  ret
@DivByZero:
  xor     eax, eax                //Here you can raise Div by 0 exception, now function only return 0.
  xor     edx, edx
  jmp     @RestoreRegisters
end;
Run Code Online (Sandbox Code Playgroud)

至少还有一种速度优化是可能的!在"巨大除数数移位优化"之后,我们可以测试除数高位,如果为0,我们不需要使用额外的bh寄存器作为第65位存储在其中.因此展开的循环部分可能如下所示:

  shl     bl,1                    //Shift dividend left for one bit
  rcl     edi,1
  rcl     esi,1
  sub     edi, ebp                //Compare dividend and  divisor
  sbb     esi, edx                //Subtract the divisor
  jnc     @NoCarryAtCmpX
  add     edi, ebp                //Return privius dividend state
  adc     esi, edx
@NoCarryAtCmpX:
Run Code Online (Sandbox Code Playgroud)


Pet*_*des 6

我知道问题指定了 32 位代码,但 64 位的答案可能对其他人有用或有趣。

是的,64b/32b => 32b 除法确实是 128b % 64b => 64b 的有用构建块。libgcc 的__umoddi3(源链接如下)给出了如何做那种事情的想法,但它只在 2N / N => N 除法之上实现了 2N % 2N => 2N,而不是 4N % 2N => 2N。

可以使用更广泛的多精度库,例如https://gmplib.org/manual/Integer-Division.html#Integer-Division


64 位机器上的 GNU C确实提供了一个__int128type和 libgcc 函数来在目标架构上尽可能有效地进行乘法和除法。

x86-64 的div r/m64指令执行 128b/64b => 64b 除法(也产生余数作为第二个输出),但如果商溢出,它会出错。所以你不能直接使用它 if A/B > 2^64-1,但你可以让 gcc 为你使用它(甚至内联 libgcc 使用的相同代码)。


这将编译(Godbolt 编译器资源管理器)成一两条div指令(发生在libgcc函数调用中)。如果有更快的方法,libgcc 可能会使用它。

#include <stdint.h>
uint64_t AmodB(unsigned __int128 A, uint64_t B) {
  return A % B;
}
Run Code Online (Sandbox Code Playgroud)

__umodti3它调用的函数计算完整的 128b/128b 模,但该函数的实现确实检查除数的高半部分为 0 的特殊情况,如您在 libgcc 源代码中所见。(libgcc 从该代码构建函数的 si/di/ti 版本,适用于目标架构。udiv_qrnnd是一个内联 asm 宏,它为目标架构 执行无符号 2N/N => N 除法。

对于 x86-64(以及其他具有硬件除法指令的体系结构),快速路径(当high_half(A) < B; 保证div不会出错时)只是两个未采用的分支,一些用于无序 CPU 咀嚼的绒毛,以及div r64根据Agner Fog 的 insn 表条指令在现代 x86 CPU 上大约需要 50-100 个周期1。其他一些工作可以与 并行发生div,但整数除法单元不是非常流水线化并且div解码为很多 uops(与 FP 除法不同)。

对于只有 64 位div的情况,回退路径仍然只使用两个 64 位指令B,但A/B不适合 64 位,因此A/B直接会出错。

请注意,libgcc__umodti3只是内联__udivmoddi4到仅返回余数的包装器中。

脚注 1:32 位div在 Intel CPU 上快 2 倍以上。在 AMD CPU 上,性能仅取决于实际输入值的大小,即使它们是 64 位寄存器中的小值。如果小值很常见,那么在进行 64 位或 128 位除法之前,可能值得将分支基准测试为简单的 32 位除法版本。


对于相同的重复模 B

这可能是值得考虑的计算定点乘法逆B,如果存在。例如,对于编译时常量,gcc 会针对小于 128b 的类型进行优化。

uint64_t modulo_by_constant64(uint64_t A) { return A % 0x12345678ABULL; }

    movabs  rdx, -2233785418547900415
    mov     rax, rdi
    mul     rdx
    mov     rax, rdx             # wasted instruction, could have kept using RDX.
    movabs  rdx, 78187493547
    shr     rax, 36            # division result
    imul    rax, rdx           # multiply and subtract to get the modulo
    sub     rdi, rax
    mov     rax, rdi
    ret
Run Code Online (Sandbox Code Playgroud)

x86 的mul r64指令做 64b*64b => 128b (rdx:rax) 乘法,并且可以用作构建块来构造 128b * 128b => 256b 乘法来实现相同的算法。由于我们只需要完整 256b 结果的高一半,因此可以节省一些乘法。

现代英特尔 CPU 具有非常高的性能mul:3c 延迟,每个时钟吞吐量一个。但是,所需的移位和加法的确切组合因常数而异,因此在运行时计算乘法逆的一般情况在每次用作 JIT 编译或静态编译版本(甚至在预计算开销之上)。

IDK 盈亏平衡点所在。对于 JIT 编译,它会高于约 200 次重用,除非您为常用B值缓存生成的代码。对于“正常”方式,它可能在 200 次重用的范围内,但 IDK 为 128 位 / 64 位除法找到模块化乘法逆是多么昂贵。

libdivide可以为您执行此操作,但仅适用于 32 位和 64 位类型。不过,这可能是一个很好的起点。