【问题标题】:Signed 64-Bit multiply and 128-Bit Divide on x86 in assembly汇编中 x86 上的有符号 64 位乘法和 128 位除法
【发布时间】:2018-07-18 01:52:56
【问题描述】:

我在我的 C++ 项目中使用了 Visual Studio 中用汇编 (masm) 编写的 2 个函数。它们是产生 128 位结果的无符号 64 位乘法函数和产生 128 位商并返回 32 位余数的无符号 128 位除法函数。

我需要的是函数的签名版本,但我不知道该怎么做。

下面是带有无符号函数的 .asm 文件的代码:

.MODEL flat, stdcall
.CODE

MUL64 PROC, A:QWORD, B:QWORD, pu128:DWORD
push EAX
push EDX
push EBX
push ECX
push EDI
mov EDI,pu128
; LO(A) * LO(B)
mov EAX,DWORD PTR A
mov EDX,DWORD PTR B
MUL EDX
mov [EDI],EAX ; Save the partial product.
mov ECX,EDX
; LO(A) * HI(B)
mov EAX,DWORD PTR A
mov EDX,DWORD PTR B+4
MUL EDX
ADD EAX,ECX
ADC EDX,0
mov EBX,EAX
mov ECX,EDX
; HI(A) * LO(B)
mov EAX,DWORD PTR A+4
mov EDX,DWORD PTR B
MUL EDX
ADD EAX,EBX
ADC ECX,EDX
PUSHFD ; Save carry.
mov [EDI+4],EAX ; Save the partial product.
; HI(A) * HI(B)
mov EAX,DWORD PTR A+4
mov EDX,DWORD PTR B+4
MUL EDX
POPFD ; Retrieve carry from above.
ADC EAX,ECX
ADC EDX,0
mov [EDI+8],EAX ; Save the partial product.
mov [EDI+12],EDX ; Save the partial product.
pop EDI
pop ECX
pop EBX
pop EDX
pop EAX
ret 20
MUL64 ENDP

IMUL64 PROC, A:SQWORD, B:SQWORD, pi128:DWORD
; How to make this work?
ret 20
IMUL64 ENDP

DIV128 PROC, pDividend128:DWORD, Divisor:DWORD, pQuotient128:DWORD
push EDX
push EBX
push ESI
push EDI
MOV ESI,pDividend128
MOV EDI,pQuotient128
MOV EBX,Divisor
XOR EDX,EDX
MOV EAX,[ESI+12]
DIV EBX
MOV [EDI+12],EAX
MOV EAX,[ESI+8]
DIV EBX
MOV [EDI+8],EAX
MOV EAX,[ESI+4]
DIV EBX
MOV [EDI+4],EAX
MOV EAX,[ESI]
DIV EBX
MOV [EDI],EAX
MOV EAX,EDX
pop EDI
pop ESI
pop EBX
pop EDX
ret 12
DIV128 ENDP

IDIV128 PROC, pDividend128:DWORD, Divisor:DWORD, pQuotient128:DWORD
; How to make this work?
ret 12
IDIV128 ENDP

END

如果您觉得这对您有帮助,请通过帮助对函数的签名版本进行编码来帮助项目。

【问题讨论】:

  • pushfd / popfd 。而且您不需要保存/恢复 EAX/ECX/EDX。 MSVC 具有 32x32 => 64 位乘法的内在函数,因此您可以用 C++ 编写它,尽管这个版本 stackoverflow.com/questions/46870373/umul128-on-windows-32-bits 尽可能高效,但仍然无法编译为良好的 asm。修复此问题以将 setc bl 的进位保存到寄存器中可能会更好。
  • 这真的是数学的应用。还记得在学校你是如何教你除符号数的吗?在这里做。
  • @AlwaysNub:popfd 是 9 uop,在 Skylake 上每 20c 吞吐量一个! (agner.org/optimize)。您可以使用add bl, 0xFF(1 uop,1c 延迟)将bl 转回CF 以设置adc。或者,如果您不需要单个 adc 的执行结果,您可以分两步完成,就像您所说的将保存的 CF 零扩展。 xor-zero ebx 在设置 CF 的 add 之前通常是基数:只有 add 在关键路径上。如果您不能保留一个寄存器,那么 setc bl / movzx ebx, bl 可能仍然比 Skylake 之前的 2-uop adc 更好。 stackoverflow.com/a/33668295
  • 是的,这些寄存器在 stdcall 和其他调用约定中被调用破坏,无论返回类型如何。您可以在godbolt.org 上查看编译器生成的简单函数代码,以了解编译器在实践中确实破坏了这些寄存器。

标签: algorithm assembly visual-c++ x86 signed


【解决方案1】:

首先,MUL64功能不能100%工作

如果你尝试做0xFFFFFFFFFFFFFFFF x 0xFFFFFFFFFFFFFFFF,Hi 64位结果是0xFFFFFFFeFFFFFFFF,应该是0xFFFFFFFFFFFFFFFe

要解决此问题,应将 POPFD 指令后的进位标志添加到 EDX,即结果的最高 32 位部分。现在按照 Peter Cordes 的建议,删除 EAX/ECX/EDX 的推送和弹出。最后使用setc BLmovzx EBX,BL 保存标志。注意:您不能轻易使用xor EBX,EBX 将其归零,因为xor 会影响标志。我们使用movzx,因为它比add BL,0xFFadd 快于基于Skylake 规范的adc

结果:

MUL64 PROC, A:QWORD, B:QWORD, pu128:DWORD
push EBX
push EDI
mov EDI,pu128
; LO(A) * LO(B)
mov EAX,DWORD PTR A
mov EDX,DWORD PTR B
mul EDX
mov [EDI],EAX ; Save the partial product.
mov ECX,EDX
; LO(A) * HI(B)
mov EAX,DWORD PTR A
mov EDX,DWORD PTR B+4
mul EDX
add EAX,ECX
adc EDX,0
mov EBX,EAX
mov ECX,EDX
; HI(A) * LO(B)
mov EAX,DWORD PTR A+4
mov EDX,DWORD PTR B
mul EDX
add EAX,EBX
adc ECX,EDX
setc BL ; Save carry.
movzx EBX,BL ; Zero-Extend carry.
mov [EDI+4],EAX ; Save the partial product.
; HI(A) * HI(B)
mov EAX,DWORD PTR A+4
mov EDX,DWORD PTR B+4
mul EDX
add EDX,EBX ; Add carry from above.
add EAX,ECX
adc EDX,0
mov [EDI+8],EAX ; Save the partial product.
mov [EDI+12],EDX ; Save the partial product.
pop EDI
pop EBX
ret 20
MUL64 ENDP

现在,要使用以下公式制作函数的签名版本:

my128.Hi -= (((A < 0) ? B : 0) + ((B < 0) ? A : 0));

结果:

IMUL64 PROC, A:SQWORD, B:SQWORD, pi128:DWORD
push EBX
push EDI
mov EDI,pi128
; LO(A) * LO(B)
mov EAX,DWORD PTR A
mov EDX,DWORD PTR B
mul EDX
mov [EDI],EAX ; Save the partial product.
mov ECX,EDX
; LO(A) * HI(B)
mov EAX,DWORD PTR A
mov EDX,DWORD PTR B+4
mul EDX
add EAX,ECX
adc EDX,0
mov EBX,EAX
mov ECX,EDX
; HI(A) * LO(B)
mov EAX,DWORD PTR A+4
mov EDX,DWORD PTR B
mul EDX
add EAX,EBX
adc ECX,EDX
setc BL ; Save carry.
movzx EBX,BL ; Zero-Extend carry.
mov [EDI+4],EAX ; Save the partial product.
; HI(A) * HI(B)
mov EAX,DWORD PTR A+4
mov EDX,DWORD PTR B+4
mul EDX
add EDX,EBX ; Add carry from above.
add EAX,ECX
adc EDX,0
mov [EDI+8],EAX ; Save the partial product.
mov [EDI+12],EDX ; Save the partial product.
; Signed version only:
cmp DWORD PTR A+4,0
jg zero_b
jl use_b
cmp DWORD PTR A,0
jae zero_b
use_b:
mov ECX,DWORD PTR B
mov EBX,DWORD PTR B+4
jmp test_b
zero_b:
xor ECX,ECX
mov EBX,ECX
test_b:
cmp DWORD PTR B+4,0
jg zero_a
jl use_a
cmp DWORD PTR B,0
jae zero_a
use_a:
mov EAX,DWORD PTR A
mov EDX,DWORD PTR A+4
jmp do_last_op
zero_a:
xor EAX,EAX
mov EDX,EAX
do_last_op:
add EAX,ECX
adc EDX,EBX
sub [EDI+8],EAX
sbb [EDI+12],EDX
; End of signed version!
pop EDI
pop EBX
ret 20
IMUL64 ENDP

DIV128 函数对于从 32 位除数获取 128 位商应该没问题(也可能是最快的),但如果您需要使用 128 位除数,请查看此代码 https://www.codeproject.com/Tips/785014/UInt-Division-Modulus使用二进制移位算法进行 128 位除法的示例。如果用汇编语言编写,它可能会快 3 倍。

要制作 DIV128 的有符号版本,首先要确定除数和被除数的符号是​​相同还是不同。如果它们相同,那么结果应该是肯定的。如果它们不同,那么结果应该是否定的。所以...如果被除数和除数为负,则将它们设为正,然后调用 DIV128,然后,如果符号不同,则否定结果。

这是一些用 C++ 编写的示例代码

VOID IDIV128(PSDQWORD Dividend, PSDQWORD Divisor, PSDQWORD Quotient, PSDQWORD Remainder)
{
    BOOL Negate;
    DQWORD DD, DV;

    Negate = TRUE;

    // Use local DD and DV so Dividend and Divisor dont get currupted.
    DD.Lo = Dividend->Lo;
    DD.Hi = Dividend->Hi;
    DV.Lo = Divisor->Lo;
    DV.Hi = Divisor->Hi;

    // if the signs are the same then: Negate = FALSE;
    if ((DD.Hi & 0x8000000000000000) == (DV.Hi & 0x8000000000000000)) Negate = FALSE;

    // Covert Dividend and Divisor to possitive if negative: (negate)
    if (DD.Hi & 0x8000000000000000) NEG128((PSDQWORD)&DD);
    if (DV.Hi & 0x8000000000000000) NEG128((PSDQWORD)&DV);

    DIV128(&DD, &DV, (PDQWORD)Quotient, (PDQWORD)Remainder);

    if (Negate == TRUE)
    {
        NEG128(Quotient);
        NEG128(Remainder);
    }
}

编辑:

按照 Peter Cordes 的建议,我们可以进一步优化 MUL64/IMUL64。查看 cmets 以了解正在进行的特定更改。我还将MUL64 PROC, A:QWORD, B:QWORD, pu128:DWORD 替换为MUL64@20:IMUL64@20:,以消除对masm 添加的EBP 的不必要使用。我还优化了 IMUL64 的标志修复工作。

MUL64/IMUL64 的当前 .asm 文件

.MODEL flat, stdcall

EXTERNDEF  MUL64@20     :PROC
EXTERNDEF  IMUL64@20    :PROC

.CODE

MUL64@20:
push EBX
push EDI

;            -----------------
;            |     pu128     |
;            |---------------|
;            |       B       |
;            |---------------|
;            |       A       |
;            |---------------|
;            |  ret address  |
;            |---------------|
;            |      EBX      |
;            |---------------|
;    ESP---->|      EDI      |
;            -----------------

A       TEXTEQU   <[ESP+12]>
B       TEXTEQU   <[ESP+20]>
pu128   TEXTEQU   <[ESP+28]>

mov EDI,pu128
; LO(A) * LO(B)
mov EAX,DWORD PTR A
mul DWORD PTR B
mov [EDI],EAX ; Save the partial product.
mov ECX,EDX
; LO(A) * HI(B)
mov EAX,DWORD PTR A
mul DWORD PTR B+4
add EAX,ECX
adc EDX,0
mov EBX,EAX
mov ECX,EDX
; HI(A) * LO(B)
mov EAX,DWORD PTR A+4
mul DWORD PTR B
add EAX,EBX
adc ECX,EDX
setc BL ; Save carry.
mov [EDI+4],EAX ; Save the partial product.
; HI(A) * HI(B)
mov EAX,DWORD PTR A+4
mul DWORD PTR B+4
add EAX,ECX
movzx ECX,BL ; Zero-Extend saved carry from above.
adc EDX,ECX
mov [EDI+8],EAX ; Save the partial product.
mov [EDI+12],EDX ; Save the partial product.
pop EDI
pop EBX
ret 20

IMUL64@20:
push EBX
push EDI

;            -----------------
;            |     pi128     |
;            |---------------|
;            |       B       |
;            |---------------|
;            |       A       |
;            |---------------|
;            |  ret address  |
;            |---------------|
;            |      EBX      |
;            |---------------|
;    ESP---->|      EDI      |
;            -----------------

A       TEXTEQU   <[ESP+12]>
B       TEXTEQU   <[ESP+20]>
pi128   TEXTEQU   <[ESP+28]>

mov EDI,pi128
; LO(A) * LO(B)
mov EAX,DWORD PTR A
mul DWORD PTR B
mov [EDI],EAX ; Save the partial product.
mov ECX,EDX
; LO(A) * HI(B)
mov EAX,DWORD PTR A
mul DWORD PTR B+4
add EAX,ECX
adc EDX,0
mov EBX,EAX
mov ECX,EDX
; HI(A) * LO(B)
mov EAX,DWORD PTR A+4
mul DWORD PTR B
add EAX,EBX
adc ECX,EDX
setc BL ; Save carry.
mov [EDI+4],EAX ; Save the partial product.
; HI(A) * HI(B)
mov EAX,DWORD PTR A+4
mul DWORD PTR B+4
add EAX,ECX
movzx ECX,BL ; Zero-Extend saved carry from above.
adc EDX,ECX
mov [EDI+8],EAX ; Save the partial product.
mov [EDI+12],EDX ; Save the partial product.
; Signed version only:
mov BL,BYTE PTR B+7
and BL,80H
jz zero_a
mov EAX,DWORD PTR A
mov EDX,DWORD PTR A+4
jmp test_a
zero_a:
xor EAX,EAX
mov EDX,EAX
test_a:
mov BL,BYTE PTR A+7
and BL,80H
jz do_last_op
add EAX,DWORD PTR B
adc EDX,DWORD PTR B+4
do_last_op:
sub [EDI+8],EAX
sbb [EDI+12],EDX
; End of signed version!
pop EDI
pop EBX
ret 20

END

【讨论】:

  • 在 Skylake 上,使用 add BL,0xFF 恢复 CF 然后使用 adc 与您正在做的事情之间是收支平衡的。只有在 Haswell 和更早的地方,adc 更贵。所以你的代码很好,但你描述的原因是错误的。
  • 还有进一步的优化可能:setc al / movzx ebx, al 允许mov-elimination for the movzx on IvyBridge and later,因为它在不同的寄存器之间。并且:代替add EDX,EBX ; carry from above. / add EAX,ECX / adc EDX,0,使用add EAX,ECX / adc EDX, EBX ; HI += CF + saved CF
  • 另外,mov edx, B / mul edx 会比mul dword ptr B 更好。如果您不打算将值保存在寄存器中,那么单独加载它没有任何好处。 Intel/AMD CPU 可以保持内存操作数微融合,这样可以为前端节省一个微指令。您没有使用esi,因此您也可以保存/恢复它,但可能不值得与多次加载相同的数据相比。并且可能也不值得在 mul / add / adc 之前启用 xor ecx,ecx 而不是之后的 movzx,因为我认为 CF 的保存/恢复不在关键延迟路径上。
  • 您可以将imul 用于HI(A) * HI(B) 部分,以节省一些标志修复工作吗?此外,这部分可能会受益于将更多输入数据保存在寄存器中而不是使用如此多的内存操作数。所以签名版本应该可以保存/恢复esiebp(如果你还没有使用ebp作为帧指针)。
  • @PeterCordes 干得好!查看我的答案的编辑以了解我的更改。我做了一些测试,我认为 imul 可以用来提供帮助。但事情可能会变得一团糟。相反,我用mov BL,BYTE PTR B+7 / and BL,80H / jz zero_a 替换了 cmp,并删除了 2 个不必要的 mov。
猜你喜欢
  • 1970-01-01
  • 2010-12-24
  • 2015-07-05
  • 2015-05-02
  • 1970-01-01
  • 1970-01-01
  • 2019-11-01
  • 2012-04-05
  • 1970-01-01
相关资源
最近更新 更多