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

时间:2010-04-02 09:54:09

标签: c algorithm x86 modulo assembly

我有一个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除以。在这种情况下,你的答案有何不同?

14 个答案:

答案 0 :(得分:30)

您可以使用Russian Peasant Multiplication的分部版本。

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

X = B;

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

while (A >= B)
{
    if (A >= X)
        A -= X;
    X >>= 1;
}

模数留在A。

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

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

答案 1 :(得分:13)

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

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

答案 2 :(得分: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

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

答案 3 :(得分: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;

至少还有一种速度优化是可能的!在“巨大除数数移位优化”之后,我们可以测试除数高位,如果为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:

答案 4 :(得分:4)

解决方案取决于您要解决的具体问题。

E.g。如果你在一个64位整数模数的环中进行算术,那么使用 Montgomerys reduction效率很高。当然,这假设您使用相同的模数很多次,并且将环的元素转换为特殊表示是值得的。


对这个Montgomerys缩减的速度进行非常粗略的估计:我有一个旧的基准测试,在2.4Ghz Core 2上以64位模数和1600 ns的指数执行模幂运算。这个取幂大约是96模块化乘法(和模块化乘法),因此每个模乘法需要大约40个周期。

答案 5 :(得分:4)

我想分享一些想法。

这并不像MSN提议的那样简单,我害怕。

在表达式中:

(((AH % B) * ((2^64 - B) % B)) + (AL % B)) % B

乘法和加法都可能溢出。我认为可以考虑到它并且仍然使用一般概念进行一些修改,但有些东西告诉我它会变得非常可怕。

我很好奇在MSVC中如何实现64位模运算,我试图找到一些东西。我真的不知道汇编,所有我可用的是Express版本,没有VC \ crt \ src \ intel \ llrem.asm的源代码,但我想我经过一些游戏后设法知道发生了什么使用调试器和反汇编输出。我试图找出在正整数和除数&gt; = 2 ^ 32的情况下如何计算余数。当然有一些代码处理负数,但我没有深入研究。

以下是我的看法:

如果除数&gt; = 2 ^ 32,则除数和除数都根据需要向右移位以使除数成为32位。换句话说:如果用二位数来将除数写成二进制并且n> 32,除数和被除数的n-32个最低有效位被丢弃。之后,使用硬件支持执行除法,将64位整数除以32位。结果可能是不正确的,但我认为可以证明,结果可能最多偏离1.在除法之后,除数(原始值)乘以结果并从除数中减去乘积。然后通过在必要时加上或减去除数来校正(如果除法的结果是1)。

很容易将128位整数除以32位,利用硬件支持64位乘32位除法。在除数&lt; 2 ^ 32,可以计算出仅执行4个分区的余数如下:

我们假设红利存储在:

DWORD dividend[4] = ...

其余部分将进入:

DWORD remainder;

1) Divide dividend[3] by divisor. Store the remainder in remainder.
2) Divide QWORD (remainder:dividend[2]) by divisor. Store the remainder in remainder.
3) Divide QWORD (remainder:dividend[1]) by divisor. Store the remainder in remainder.
4) Divide QWORD (remainder:dividend[0]) by divisor. Store the remainder in remainder.

在这4个步骤之后,变量余数将保留您正在寻找的内容。 (如果我的结局错了,请不要杀我。我甚至不是程序员)

如果除数大于2 ^ 32-1,我没有好消息。在我之前描述的程序中,我认为MSVC正在使用的程序中没有完整的证据证明移位后的结果不超过1。然而,我认为它与事实有关,被丢弃的部分至少比除数小2 ^ 31倍,被除数小于2 ^ 64且除数大于2 ^ 32-1 ,结果小于2 ^ 32。

如果被除数为128位,则丢弃位的技巧将不起作用。因此,一般情况下,最佳解决方案可能是GJ或caf提出的解决方案。 (好吧,即使丢弃比特工作也可能是最好的。对128位整数进行除法,乘法减法和校正可能会更慢。)

我还在考虑使用浮点硬件。 x87浮点单元使用80位精度格式,分数为64位长。我认为可以通过64位除法得到64位的确切结果。 (不是直接的余数,而是使用乘法和减法的余数,如“MSVC程序”)。如果被除数> = 2 ^ 64且&lt;以浮点格式存储它的2 ^ 128类似于丢弃“MSVC过程”中的最低有效位。也许有人可以证明这种情况下的错误是绑定的并且发现它很有用。我不知道它是否有机会比GJ的解决方案更快,但也许值得尝试。

答案 6 :(得分:3)

我已经制作了两个版本的Mod128by64'俄罗斯农民'分区功能:经典和速度优化。速度优化可以在我的3Ghz PC上实现每秒超过1000.000次随机计算,比经典功能快三倍以上。 如果我们比较计算128乘64的执行时间和64乘64位模数计算64比这个函数慢大约50%。

经典的俄罗斯农民:

function Mod128by64Clasic(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
//   : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//edx:ebp = Divisor
//ecx = Loop counter
//Result = esi:edi
  push    ebx                     //Store registers to stack
  push    esi
  push    edi
  push    ebp
  mov     ebp, [edx]              //Load  divisor to edx:ebp
  mov     edx, [edx + 4]
  mov     ecx, ebp                //Div by 0 test
  or      ecx, edx
  jz      @DivByZero
  push    [eax]                   //Store Divisor to the stack
  push    [eax + 4]
  push    [eax + 8]
  push    [eax + 12]
  xor     edi, edi                //Clear result
  xor     esi, esi
  mov     ecx, 128                //Load shift counter
@Do128BitsShift:
  shl     [esp + 12], 1           //Shift dividend from stack left for one bit
  rcl     [esp + 8], 1
  rcl     [esp + 4], 1
  rcl     [esp], 1
  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:
  loop    @Do128BitsShift
//End of 128 bit division loop
  mov     eax, edi                //Load result to eax:edx
  mov     edx, esi
@RestoreRegisters:
  lea     esp, esp + 16           //Restore Divisors space on stack
  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;

速度优化的俄罗斯农民:

function Mod128by64Oprimized(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
//   : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//Divisor = edx:ebp
//Dividend = ebx:edx //We need 64 bits
//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 Compute 8 Bits unroled 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 bit part of Dividend
//Compute 8 Bits unroled loop
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove0         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow0
  ja      @DividentAbove0
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow0
@DividentAbove0:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow0:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove1         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow1
  ja      @DividentAbove1
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow1
@DividentAbove1:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow1:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove2         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow2
  ja      @DividentAbove2
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow2
@DividentAbove2:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow2:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove3         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow3
  ja      @DividentAbove3
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow3
@DividentAbove3:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow3:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove4         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow4
  ja      @DividentAbove4
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow4
@DividentAbove4:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow4:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove5         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow5
  ja      @DividentAbove5
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow5
@DividentAbove5:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow5:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove6         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow6
  ja      @DividentAbove6
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow6
@DividentAbove6:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow6:
  shl     bl, 1                   //Shift dividend left for one bit
  rcl     edi, 1
  rcl     esi, 1
  jc      @DividentAbove7         //dividend hi bit set?
  cmp     esi, edx                //dividend hi part larger?
  jb      @DividentBelow7
  ja      @DividentAbove7
  cmp     edi, ebp                //dividend lo part larger?
  jb      @DividentBelow7
@DividentAbove7:
  sub     edi, ebp                //Return privius dividend state
  sbb     esi, edx
@DividentBelow7:
//End of Compute 8 Bits (unroled loop)
  dec     cl                      //Decrement byte loop shift counter
  jns     @Shift8Bits             //Last jump at cl = 0!!!
//End of 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;

答案 7 :(得分:3)

@caf接受的答案非常好,评价很高,但它包含了多年未见的错误。

为了帮助测试该解决方案和其他解决方案,我发布了测试工具并将其作为社区维基。

unsigned cafMod(unsigned A, unsigned B) {
  assert(B);
  unsigned X = B;
  // while (X < A / 2) {  Original code used <
  while (X <= A / 2) {
    X <<= 1;
  }
  while (A >= B) {
    if (A >= X) A -= X;
    X >>= 1;
  }
  return A;
}

void cafMod_test(unsigned num, unsigned den) {
  if (den == 0) return;
  unsigned y0 = num % den;
  unsigned y1 = mod(num, den);
  if (y0 != y1) {
    printf("FAIL num:%x den:%x %x %x\n", num, den, y0, y1);
    fflush(stdout);
    exit(-1);
  }
}

unsigned rand_unsigned() {
  unsigned x = (unsigned) rand();
  return x * 2 ^ (unsigned) rand();
}

void cafMod_tests(void) {
  const unsigned i[] = { 0, 1, 2, 3, 0x7FFFFFFF, 0x80000000, 
      UINT_MAX - 3, UINT_MAX - 2, UINT_MAX - 1, UINT_MAX };
  for (unsigned den = 0; den < sizeof i / sizeof i[0]; den++) {
    if (i[den] == 0) continue;
    for (unsigned num = 0; num < sizeof i / sizeof i[0]; num++) {
      cafMod_test(i[num], i[den]);
    }
  }
  cafMod_test(0x8711dd11, 0x4388ee88);
  cafMod_test(0xf64835a1, 0xf64835a);

  time_t t;
  time(&t);
  srand((unsigned) t);
  printf("%u\n", (unsigned) t);fflush(stdout);
  for (long long n = 10000LL * 1000LL * 1000LL; n > 0; n--) {
    cafMod_test(rand_unsigned(), rand_unsigned());
  }

  puts("Done");
}

int main(void) {
  cafMod_tests();
  return 0;
}

答案 8 :(得分:3)

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

是的,64b / 32b =&gt; 32b除法确实为128b%构成了一个有用的构建块64b =&gt; 64B。 libgcc的__umoddi3(下面链接的源代码)给出了如何做这种事情的想法,但它只实现2N%2N =&gt;在2N / N = 1之上的2N; N分度,而不是4N%2N =&gt; 2N。

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

64位计算机上的GNU C 确实提供了__int128 type和libgcc函数,以便在目标体系结构上尽可能高效地进行乘法和除法。

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

这会将(Godbolt compiler explorer)编译为一个或两个div指令(发生在libgcc函数调用中)。如果有更快的方法,libgcc可能会使用它。

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

它调用的__umodti3函数计算一个完整的128b / 128b模数,但该函数的实现会检查除数的高半部分为0的特殊情况,你可以see in the libgcc source。 (libgcc根据该代码构建函数的si / di / ti版本,适用于目标体系结构。udiv_qrnnd是一个内联asm宏,为目标体系结构执行无符号2N / N =&gt; N除法。

对于x86-64 (以及其他具有硬件除法指令的体系结构),快速路径high_half(A) < B时;保证div不会出错)只是两个未被采取的分支,一些无序的CPU可以通过,和一条div r64指令,这需要大约50根据{{​​3}},现代x86 CPU上的-100个周期。其他一些工作可以与div并行进行,但是整数除法单元不是非常流水线的,div解码为很多uops(与FP除法不同)。

对于div仅为64位但B不适合64位的情况,回退路径仍然只使用两条64位A/B指令{{1>} A/B 1}}直接就会出错。

请注意,libgcc的__umodti3只是将__udivmoddi4内联到一个只返回余数的包装器中。

由同一B

重复模数

如果存在,B计算Agner Fog's insn tables可能值得考虑。例如,对于编译时常量,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

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

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

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

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

答案 9 :(得分:2)

作为一般规则,除法很慢,乘法更快,而且位移更快。从我所看到的答案到目前为止,大多数答案一直使用蛮力方法使用位移。还有另一种方式。是否更快还有待观察(AKA简介)。

而不是分割,乘以倒数。因此,为了发现A%B,首先计算B ... 1 / B的倒数。这可以使用Newton-Raphson收敛方法通过几个循环完成。要做到这一点,将取决于表中的一组良好的初始值。

有关收敛于倒数的Newton-Raphson方法的更多详细信息,请参阅http://en.wikipedia.org/wiki/Division_(digital)

一旦你有了倒数,商Q = A * 1 / B.

余数R = A - Q * B.

要确定这是否会比蛮力更快(因为我们将使用32位寄存器来模拟64位和128位数字,因此将会有更多的乘法,请对其进行分析。

如果B在代码中是常数,则可以预先计算倒数,并使用最后两个公式进行简单计算。这一点,我肯定会比位移更快。

希望这有帮助。

答案 10 :(得分:1)

如果您有最新的x86机器,则SSE2 +有128位寄存器。我从来没有试过为基本的x86以外的任何东西编写程序集,但我怀疑那里有一些指南。

答案 11 :(得分:1)

如果通过63位无符号的128位无符号足够好,则可以在最多63个周期的循环中完成。

考虑这个建议的解决方案MSN的溢出问题,将其限制为1位。我们通过将问题分成2,模块化乘法并在最后添加结果来实现。

在下面的示例中,upper对应于最高有效64位,低到最低有效64位,div是除数。

unsigned 128_mod(uint64_t upper, uint64_t lower, uint64_t div) {
  uint64_t result = 0;
  uint64_t a = (~0%div)+1;
  upper %= div; // the resulting bit-length determines number of cycles required

  // first we work out modular multiplication of (2^64*upper)%div
  while (upper != 0){
    if(upper&1 == 1){
      result += a;
      if(result >= div){result -= div;}
    }
    a <<= 1;
    if(a >= div){a -= div;}
    upper >>= 1;
  }

  // add up the 2 results and return the modulus
  if(lower>div){lower -= div;}
  return (lower+result)%div;
}

唯一的问题是,如果除数是64位,那么我们会得到1位溢出(信息丢失),从而产生错误结果。

我不知道我还没有想出一个处理溢出的简洁方法。

答案 12 :(得分:1)

我不知道如何编译汇编代码,任何帮助编译和测试它们的帮助都将不胜感激。

我通过与 gmplib "mpz_mod()" 进行比较并总结 100 万个循环结果来解决这个问题。从减速 (seedup 0.12) 到加速 1.54 需要很长时间——这就是我认为这个线程中的 C 代码会很慢的原因。

此线程中包含测试工具的详细信息:
https://www.raspberrypi.org/forums/viewtopic.php?f=33&t=311893&p=1873122#p1873122

这是“mod_256()”,比使用 gmplib“mpz_mod()”有加速,使用 __builtin_clzll() 进行更长的转换是必不可少的:

typedef __uint128_t uint256_t[2];

#define min(x, y) ((x<y) ? (x) : (y))

int clz(__uint128_t u)
{
//  unsigned long long h = ((unsigned long long *)&u)[1];
  unsigned long long h = u >> 64;
  return (h!=0) ? __builtin_clzll(h) : 64 + __builtin_clzll(u);
}

__uint128_t mod_256(uint256_t x, __uint128_t n)
{
  if (x[1] == 0)  return x[0] % n;
  else
  {
    __uint128_t r = x[1] % n;
    int F = clz(n);
    int R = clz(r);
    for(int i=0; i<128; ++i)
    {
      if (R>F+1)
      {
        int h = min(R-(F+1), 128-i);
        r <<= h; R-=h; i+=(h-1); continue;
      }
      r <<= 1; if (r >= n)  { r -= n; R=clz(r); }
    }
    r += (x[0] % n); if (r >= n)  r -= n;

    return r;
  }
}

答案 13 :(得分:-2)

由于C中没有预定义的128位整数类型,因此A的位必须以数组表示。虽然B(64位整数)可以存储在 unsigned long long int 变量中,但是需要将B位放入另一个数组中,以便有效地处理A和B.

之后,B增加为Bx2,Bx3,Bx4,......直到它是最小B小于A.然后(A-B)可以使用基数2的一些减法知识来计算。

这是您正在寻找的解决方案吗?