【问题标题】:How do I implement an efficient 32 bit DivMod in 64 bit code如何在 64 位代码中实现高效的 32 位 DivMod
【发布时间】:2013-11-28 16:11:25
【问题描述】:

我想使用一个专门在 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 位代码是否有性能优势。这甚至可能吗?还是我会简单地使用UInt64 参数重新实现DivMod 重载?如果值得实现一个定制的 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;

【问题讨论】:

  • 看起来不错。比 purepascal 版本快大约 33%。
  • @LURD 你知道上面代码中的DIV ECX 是否执行32位操作吗?我理解对了吗?它是否对 64 位值 EDX:EXA 除以 ECX 进行无符号除法?是否值得做 32 位操作而不是完整的 64 位操作?
  • 64-bit 标记替换为x86-64 标记,因为该问题特定于x86-64,而不是一般的64 位架构。
  • @DavidHeffernan 如果您在编译时知道除数,则整数除法可以加快很多(通过魔术常量和以便宜的mul 交易divshr)。签出:libdivide.com 还有intel.com/content/dam/www/public/us/en/documents/manuals/… 的第 9.2.4 节,一些编译器会自动执行此操作 - delphi 似乎没有。
  • @J... 0.1 不可表示。您的代码可能更快,但它是错误的。当然。

标签: delphi assembly x86-64


【解决方案1】:

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

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 不错。谢谢。我会调查一下。当然,获得余数的方式也是避免纯帕斯卡版本中的第二次除法的一种巧妙方法。我的意思是:Quotient := Dividend div Divisor; Remainder := Dividend - Quotient*Divisor;
  • @DavidHeffernan 这是直接算术算法。围绕 libdivide 抛出一个 DLL 包装器将提供一些使用 SSE/SSE2 的其他工具(div 10 的未切换矢量算法甚至比这更快!)。这个实现可以快速移植,但还有其他实现。
  • 为了清楚起见,我不会接受这个答案,因为它没有直接解决我提出的问题。它确实为我试图解决的问题提供了一个很好的解决方案。希望你能理解。无论如何,这很有趣!
  • 有趣的是,32 位代码中的无分割版本更慢!这大概是因为 64 位乘法很昂贵,而硬件不支持它。
  • @DavidHeffernan 是的,我同意它不能回答一般问题(尽管完整的 libdivide 库确实提供了一个完整的、优化的通用运行时解决方案,尽管它在 C/C++ 中)。正如您所说,我还注意到它在 32 位中速度较慢,因为 64 位 IMUL 本身不可用。为 SSE/SSE2 重新工具将重新获得 32 位编译的速度(我猜,即使 asm 有强制性的堆栈帧开销)。 Libdivide 会自动执行此操作(尽管对于编译时已知的除数,硬编码幻数而不是动态计算它仍然更快)。
【解决方案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:EAX 除以 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:一个:= 666; 0000000000423A68 C7450C9A020000 mov [rbp+$0c],$0000029a 项目 39.dpr.15:b := 42; 0000000000423A6F C745082A000000 mov [rbp+$08],$0000002a Project39.dpr.16: c := a div b; 0000000000423A76 8B450C 移动 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 移动 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 位操作似乎更自然。

【讨论】:

  • “加宽” DivMod 原型应该是(QWord, LongWord, &amp;LongWord, &amp;LongWord)
  • @Free 对于我的需要,需要 32 位操作数。 QWORD红利与硬件能力相契合。
猜你喜欢
  • 2016-01-02
  • 1970-01-01
  • 2021-02-08
  • 2012-01-19
  • 1970-01-01
  • 2017-07-22
  • 2011-05-03
  • 2013-02-06
  • 2017-02-18
相关资源
最近更新 更多