如何在64位代码中实现高效的32位DivMod

时间:2013-11-28 16:11:26

标签: delphi assembly x86-64

我想使用专门针对32位操作数运行的DivMod函数。 implementation in the RTL返回16位变量中的值。它的声明是:

procedure DivMod(Dividend: Cardinal; Divisor: Word; var Result, Remainder: Word);

所以,我不能使用它,因为我的输入可能会溢出返回值。

天真的Pascal实现如下所示:

procedure DivMod(Dividend, Divisor: Cardinal; out Quotient, Remainder: Cardinal);
begin
  Quotient := Dividend div Divisor;
  Remainder := Dividend mod Divisor;
end;

这很有效,但两次进行分组。由于我的代码的一部分调用了函数,这是一个性能瓶颈,我只想执行一次除法。为此,我使用了Serg的32位DivMod来自这个问题:Is there a DivMod that is *not* Limited to Words (<=65535)?

procedure DivMod(Dividend, Divisor: Cardinal; out Quotient, Remainder: Cardinal);
asm
        PUSH EBX
        MOV  EBX,EDX
        XOR  EDX,EDX
        DIV  EBX
        MOV  [ECX],EAX
        MOV  EBX,Remainder
        MOV  [EBX],EDX
        POP  EBX
end;

这很有效。

但是现在我想要一个64位代码的函数版本。请注意,我仍然希望对32位操作数进行操作,并返回32位值。

我应该使用64位汇编程序重写函数,还是使用运行的RTL的DivMod重载并返回64位值就足够了?

具体来说,我想知道在编写执行32位操作的64位代码时是否有性能优势。这甚至可能吗?或者我是否会最终使用DivMod参数重新实施UInt64重载?如果值得实现一个定制的64位asm版本,我将如何去做,注意操作数和操作是32位。

我认为它看起来像这样,但我不是专家,可能会出错:

procedure DivMod(Dividend, Divisor: Cardinal; out Quotient, Remainder: Cardinal);
asm
        MOV   EAX,ECX   // move Dividend to EAX
        MOV   ECX,EDX   // move Divisor to ECX
        XOR   EDX,EDX   // zeroise EDX
        DIV   ECX       // divide EDX:EAX by ECX
        MOV   [R8],EAX  // save quotient
        MOV   [R9],EDX  // save remainder
end;

2 个答案:

答案 0 :(得分:7)

对于总是除以10(每条评论)的特殊情况,您可以执行以下操作:

procedure DivMod10(num : Cardinal; var q, r : Cardinal); inline;
var
  rl : uInt64;
begin
  rl := UInt64(3435973837)*num;
  q := rl shr 35;
  r := num - q*10;
end;

算法因分母而异,但确定它的来源和幻数可以在libdivide中找到。这对于所有无符号32位整数都是准确的,并且比使用div快3倍(并提供余数)。

基准(优化):

  t0 := GetTickCount;
  for I := 1 to 999999999 do begin
    DivMod10(i, q, r);
  end;
  ShowMessage(IntToStr(GetTickCount - t0));  // result :  1809

  t0 := GetTickCount;
  for I := 1 to 999999999 do begin
    q := i div 10;
  end;
  ShowMessage(IntToStr(GetTickCount - t0));  // result :  5336

测试:

for I := 1 to High(Cardinal) do begin
  DivMod10(i,q,r);
  if q <> (i div 10) then WriteLn(IntToStr(i));
  // no mismatch found
end;

答案 1 :(得分:2)

我挖得更深了一点。我认为在UInt64版本之上实现它是完全合理的。这看起来像这样:

procedure DivMod(Dividend, Divisor: Cardinal; out Quotient, Remainder: Cardinal);
var
  Quotient64, Remainder64: UInt64;
begin
  DivMod(Dividend, Divisor, Quotient64, Remainder64);
  Quotient := Quotient64;
  Remainder := Remainder64;
end;

与最佳asm版本相比,我认为性能不会受到太大影响。

但是,我认为问题中的x64 asm代码是正确的。 MOV指令对32位操作数都很好。并且DIV也如asm代码中的注释中所述。 DIV r/m32的英特尔documentation说:

  

无符号除法EDX:E / r / m32,结果存储在EAX←商,EDX←剩余。

让我们看一下Delphi编译器对此代码的作用:

var
  a, b, c, d: Cardinal;
....
a := 666;
b := 42;
c := a div b;
d := a mod b;

生成的代码是:

    
Project39.dpr.14: a := 666;
0000000000423A68 C7450C9A020000   mov [rbp+$0c],$0000029a
Project39.dpr.15: b := 42;
0000000000423A6F C745082A000000   mov [rbp+$08],$0000002a
Project39.dpr.16: c := a div b;
0000000000423A76 8B450C           mov eax,[rbp+$0c]
0000000000423A79 33D2             xor edx,edx
0000000000423A7B F77508           div dword ptr [rbp+$08]
0000000000423A7E 894504           mov [rbp+$04],eax
Project39.dpr.17: d := a mod b;
0000000000423A81 8B450C           mov eax,[rbp+$0c]
0000000000423A84 33D2             xor edx,edx
0000000000423A86 F77508           div dword ptr [rbp+$08]
0000000000423A89 895500           mov [rbp+$00],edx

我没有任何期望32位除法比64位除法更有效,但这并不重要。使用32位操作数执行32位操作似乎更自然。