到目前为止,您可以做的最重要的优化是使用定点乘法逆除以编译时常数:Why does GCC use multiplication by a strange number in implementing integer division?。
任何体面的 C 编译器都会为您做到这一点,但显然 Delphi 不会,因此使用 asm 这样做是有充分理由的。
您可以在 EAX 中返回一个值,而不是将商和余数都存储到内存中吗?传递 2 个指针参数并强制调用者从内存中检索值似乎是一种浪费。 (更新,是的,我认为您可以将其设为函数而不是过程;不过,我只是盲目地从其他答案中修改 Delphi 代码。)
无论如何,幸运的是,我们可以使用 C 编译器来完成为我们计算乘法逆和移位计数的艰苦工作。我们甚至可以让它使用与 Delphi 用于内联汇编的相同“调用约定”。 GCC's regparm=3 32-bit calling convention 在 EAX、EDX 和 ECX 中传递 args(按此顺序)。
对于只需要商的情况,您可能需要制作一个单独的版本,因为(与慢速 div 指令不同),如果您使用的是快速乘法,则必须将余数单独计算为 x - (x/y)*y逆。但是,是的,这仍然是现代 x86 的两倍到 4 倍。
或者你可以让余数计算在纯 Delphi 中完成,除非编译器在一般优化方面很糟糕。
#ifdef _MSC_VER
#define CONVENTION _fastcall // not the same, but 2 register args are better than none.
#else
#define CONVENTION __attribute__((regparm(3)))
#endif
// use gcc -Os to get it to emit code with actual div.
divmod10(unsigned x, unsigned *quot, unsigned *rem) {
unsigned tmp = x/10;
// *quot = tmp;
*rem = x%10;
return tmp;
}
From the Godbolt compiler explorer:
# gcc8.2 -O3 -Wall -m32
div10: # simplified version without the remainder, returns in EAX
mov edx, -858993459 # 0xCCCCCCCD
mul edx # EDX:EAX = dividend * 0xCCCCCCCD
mov eax, edx
shr eax, 3
ret
# quotient in EAX
# returns quotient in EAX, stores remainder to [ECX]
# quotient pointer in EDX is unused (and destroyed).
divmod10:
mov edx, -858993459
push ebx
mov ebx, eax
mul edx # EDX:EAX = dividend * 0xCCCCCCCD
mov eax, edx
shr eax, 3
# quotient in EAX = high_half(product) >> 3 = product >> (32+3)
lea edx, [eax+eax*4] # EDX = quotient*5
add edx, edx # EDX = quot * 10
sub ebx, edx # remainder = dividend - quot*10
mov DWORD PTR [ecx], ebx # store remainder
pop ebx
ret
# quotient in EAX
这是 C 编译器的输出。根据需要适应 Delphi 内联汇编;我认为输入位于正确的 Delphi 寄存器中。
如果 Delphi inline-asm 不允许您破坏 EDX,您可以保存/恢复它。或者你想去掉未使用的quotient指针输入,然后你可以调整asm,或者调整Godbolt上的C,看看新的编译器输出。
这比div 的指令多,但div 非常慢(10 微秒,即使在 Skylake 上也有 26 个周期延迟。)
如果你在 Delphi 中有 64 位整数类型,你可以在 Delphi 源代码中这样做,避免内联 asm。或者如 MBo 所示,您可以使用 $CCCD 作为仅使用 32 位整数类型的 0..2^16-1 范围内的输入的乘法逆元。
对于其余部分,存储/重新加载往返(4 到 5 个周期)与最近的英特尔 CPU 上的实际计算具有相似的延迟,使用 mov-elimination(3 + 1 到商,另外 3 用于 lea/add /sub = 7),因此必须为此使用内联 asm 是很垃圾的。但在延迟和吞吐量方面,它仍然比div 指令要好。请参阅https://agner.org/optimize/ 和其他性能链接in the x86 tag wiki。
可以复制/粘贴的Delphi版本
(如果我做对了,我不知道 Delphi,只是根据我对调用约定/语法的推断,在 SO 和 this site 上复制+修改示例 )
我不确定我是否获得了 inline-asm 的 arg 传递权。 This RADStudio documentation 说“除了 ESP 和 EBP,asm 语句不能假设任何关于进入语句的寄存器内容。”但我假设 args 在 EAX 和 EDX 中。
将 asm 用于 64 位代码可能很愚蠢,因为在 64 位中,您可以有效地使用纯 Pascal 进行 64 位乘法。 How do I implement an efficient 32 bit DivMod in 64 bit code。所以在{$IFDEF CPUX64} 块中,最好的选择可能是使用UInt64(3435973837)*num; 的纯帕斯卡
function Div10(Num: Cardinal): Cardinal;
{$IFDEF PUREPASCAL}
begin
Result := Num div 10;
end;
{$ELSE !PUREPASCAL}
{$IFDEF CPUX86}
asm
MOV EDX, $CCCCCCCD
MUL EDX // EDX:EAX = Num * fixed-point inverse
MOV EAX,EDX // mov then overwrite is ideal for Intel mov-elimination
SHR EAX,3
end;
{$ENDIF CPUX86}
{$IFDEF CPUX64}
asm
// TODO: use pure pascal for this; Uint64 is efficient on x86-64
// Num in ECX, upper bits of RCX possibly contain garbage?
mov eax, ecx // zero extend Num into RAX
mov ecx, $CCCCCCCD // doesn't quite fit in a sign-extended 32-bit immediate for imul
imul rax, rcx // RAX = Num * fixed-point inverse
shr rax, 35 // quotient = eax
end;
{$ENDIF CPUX64}
{$ENDIF}
{Remainder is the function return value}
function DivMod10(Num: Cardinal; var Quotient: Cardinal): Cardinal;
{$IFDEF PUREPASCAL}
begin
Quotient := Num div 10;
Result := Num mod 10;
end;
{$ELSE !PUREPASCAL}
{$IFDEF CPUX86}
asm
// Num in EAX, @Quotient in EDX
push esi
mov ecx, edx // save @quotient
mov edx, $CCCCCCCD
mov esi, eax // save dividend for use in remainder calc
mul edx // EDX:EAX = dividend * 0xCCCCCCCD
shr edx, 3 // EDX = quotient
mov [ecx], edx // store quotient into @quotient
lea edx, [edx + 4*edx] // EDX = quot * 5
add edx, edx // EDX = quot * 10
mov eax, esi // off the critical path
sub eax, edx // Num - (Num/10)*10
pop esi
// Remainder in EAX = return value
end;
{$ENDIF CPUX86}
{$IFDEF CPUX64}
asm
// TODO: use pure pascal for this? Uint64 is efficient on x86-64
// Num in ECX, @Quotient in RDX
mov r8d, ecx // zero-extend Num into R8
mov eax, $CCCCCCCD
imul rax, r8
shr rax, 35 // quotient in eax
lea ecx, [rax + 4*rax]
add ecx, ecx // ecx = 10*(Num/10)
mov [rdx], eax // store quotient
mov eax, r8d // copy Num again
sub eax, ecx // remainder = Num - 10*(Num/10)
// we could have saved 1 mov instruction by returning the quotient
// and storing the remainder. But this balances latency better.
end;
{$ENDIF CPUX64}
{$ENDIF}
存储商并返回余数意味着两者可能在调用者中几乎同时准备好,因为从商计算余数的额外延迟与存储转发重叠。 IDK 如果这很好,或者如果在基于商的某些工作上开始乱序执行通常会更好。我猜如果你调用 DivMod10,你可能只想要剩下的。
但是在重复除以 10 的拆分为十进制数字的循环中,形成关键路径的是商,因此返回商并存储余数的版本会更好在那里选择。
在这种情况下,您可以将商设为 EAX 中的返回值,并将函数 arg 重命名为余数。
asm 基于此版本的 C 函数 (https://godbolt.org/z/qu2kvV) 的 clang 输出,针对 Windows x64 调用约定。但是进行了一些调整以使其更有效,例如将mov 关闭关键路径,并使用不同的寄存器来避免 REX 前缀。并用一个 ADD 替换一个 LEA。
unsigned divmod10(unsigned x, unsigned *quot) {
unsigned qtmp = x/10;
unsigned rtmp = x%10;
*quot = qtmp;
//*rem = rtmp;
return rtmp;
}
我使用 clang 的版本而不是 gcc 的版本,因为 imul r64,r64 在 Intel CPU 和 Ryzen 上更快(3 个周期延迟/1 uop)。 mul r32 是 3 uop,在 Sandybridge 系列上每 2 个时钟吞吐量只有 1 个。我认为乘法硬件自然会产生 128 位的结果,而将其中的低 64 位拆分为 edx:eax 需要额外的 uop 或类似的东西。