繁体   English   中英

add vs mul(IA32-Assembly)

[英]add vs mul (IA32-Assembly)

我知道与mul函数相比, add更快。

我想知道如何在下面的代码中使用add而不是mul来提高效率。

示例代码:

            mov eax, [ebp + 8]              #eax = x1
            mov ecx, [ebp + 12]             #ecx = x2
            mov edx, [ebp + 16]             #edx = y1
            mov ebx, [ebp + 20]             #ebx = y2

            sub eax,ecx                     #eax = x1-x2
            sub edx,ebx                     #edx = y1-y2

            mul edx                         #eax = (x1-x2)*(y1-y2)

addmul快,但是如果你想要乘以两个通用值, mul比任何循环迭代添加操作要快得多。

您不能认真使用add来使代码变得比使用mul更快。 如果你需要乘以一些小的常数值(比如2),那么也许你可以使用add来加快速度。 但对于一般情况 - 没有。

如果要将两个您事先不知道的值相乘,则实际上不可能超过x86汇编程序中的乘法指令。

如果您事先知道其中一个操作数的值,则可以通过使用少量添加来击败乘法指令。 当已知操作数很小并且在其二进制表示中仅具有几个位时,这尤其有效。 要将未知值x乘以包含2 ^ p + 2 ^ q + ... 2 ^ r的已知值,您只需添加x * 2 ^ p + x * 2 ^ q + .. x * 2 * r如果位p,q ,...和r已设定。 这可以通过左移和添加在汇编程序中轻松完成:

;  x in EDX
;  product to EAX
xor  eax,eax
shl  edx,r ; x*2^r
add  eax,edx
shl  edx,q-r ; x*2^q
add  eax,edx
shl  edx,p-q ; x*2^p
add  eax,edx

这个问题的关键问题是,假设超标量CPU受寄存器依赖性约束,它至少需要4个时钟才能完成。 乘法在现代CPU上通常需要10个或更少的时钟,如果这个序列比时间长,你也可以进行乘法运算。

乘以9:

mov  eax,edx ; same effect as xor eax,eax/shl edx 1/add eax,edx
shl  edx,3 ; x*2^3
add  eax,edx

这节拍倍增; 应该只需2个时钟。

不太为人所知的是使用LEA(加载有效地址)指令来实现快速乘以小常数。 LEA只采用单个时钟最坏的情况,其执行时间通常可以通过超标量CPU与其他指令重叠。

LEA本质上是“用小常数乘法器加两个值”。 它计算t = 2 ^ k * x + y为k = 1,2,3(参见英特尔参考手册),t,x和y为任何寄存器。 如果x == y,则可以获得1,2,3,4,5,8,9倍x,但使用x和y作为单独的寄存器允许将中间结果组合移动到其他寄存器(例如,到t) ),结果非常方便。 使用它,您可以使用单个指令完成乘以9:

lea  eax,[edx*8+edx]  ; takes 1 clock

仔细使用LEA,您可以在少数周期中乘以各种特殊常数:

lea  eax,[edx*4+edx] ; 5 * edx
lea  eax,[eax*2+edx] ; 11 * edx
lea  eax,[eax*4] ; 44 * edx

要做到这一点,你必须将你的常数乘数分解为涉及1,2,3,4,5,8和9的各种因子/总和。值得注意的是你可以做多少小常数,并且仍然只使用3- 4条说明。

如果允许使用其他典型的单时钟指令(例如,SHL / SUB / NEG / MOV),则可以乘以纯LEA无法自行完成的某些常数值。 乘以31:

lea  eax,[4*edx]
lea  eax,[8*eax]  ; 32*edx
sub  eax,edx; 31*edx ; 3 clocks

相应的LEA序列更长:

lea  eax,[edx*4+edx]
lea  eax,[edx*2+eax] ; eax*7
lea  eax,[eax*2+edx] ; eax*15
lea  eax,[eax*2+edx] ; eax*31 ; 4 clocks

弄清楚这些序列有点棘手,但您可以设置有组织的攻击。

由于LEA,SHL,SUB,NEG,MOV都是最差情况下的单时钟指令,如果它们不依赖于其他指令,则零时钟,您可以计算任何此类序列的执行成本。 这意味着您可以实现动态编程算法,以生成此类指令的最佳序列。 这仅在时钟计数小于特定CPU的整数乘法时才有用(我使用5个时钟作为经验法则), 并且它不会耗尽所有寄存器,或者至少它不会使用寄存器已经很忙(避免任何溢出)。

我实际上将它构建到我们的PARLANSE编译器中,它非常有效地计算结构A [i]的数组的偏移量,其中A中结构元素的大小是已知常量。 一个聪明的人可能会缓存答案,因此每次乘以相同的常数时都不必重新计算; 我实际上并没有这样做,因为生成此类序列的时间少于您的预期。

有趣的是打印出所有常数乘以1到10000所需的指令序列。大多数指令可以在最坏情况下的5-6指令中完成。 因此,PARLANSE编译器甚至在索引甚至最糟糕的嵌套结构数组时也几乎不使用实际的乘法。

除非你的乘法是相当简单的add很可能不会跑赢一个mul 话虽如此,你可以使用add来做乘法:

Multiply by 2:
    add eax,eax          ; x2
Multiply by 4:
    add eax,eax          ; x2
    add eax,eax          ; x4
Multiply by 8:
    add eax,eax          ; x2
    add eax,eax          ; x4
    add eax,eax          ; x8

他们很适合两个人的力量。 我不是说他们更快。 在花哨的乘法指令之前的几天,它们肯定是必要的。 这是来自一个人的灵魂是在地狱火中伪造的人,那就是Mostek 6502,Zilog z80和RCA1802 :-)

您甚至可以通过简单地存储中间结果来乘以非权力:

Multiply by 9:
    push ebx              ; preserve
    push eax              ; save for later
    add  eax,eax          ; x2
    add  eax,eax          ; x4
    add  eax,eax          ; x8
    pop  ebx              ; get original eax into ebx
    add  eax,ebx          ; x9
    pop  ebx              ; recover original ebx

我通常建议您编写代码主要是为了提高可读性,并且只在需要时担心性能。 但是,如果您在汇编程序中工作,那么您可能已经那时。 但我不确定我的“解决方案”是否真的适用于你的情况,因为你有一个任意的被乘数。

但是,您应该始终在目标环境中分析您的代码,以确保您正在执行的操作实际上更快。 汇编程序根本不会改变优化的那个方面。


如果你真的想看到一些更通用的汇编程序的使用add做乘法,这里是一个将采取两个无符号值的例行axbx ,作为回报,该产品ax 它不会优雅地处理溢出。

START:  MOV    AX, 0007    ; Load up registers
        MOV    BX, 0005
        CALL   MULT        ; Call multiply function.
        HLT                ; Stop.

MULT:   PUSH   BX          ; Preserve BX, CX, DX.
        PUSH   CX
        PUSH   DX

        XOR    CX,CX       ; CX is the accumulator.

        CMP    BX, 0       ; If multiplying by zero, just stop.
        JZ     FIN

MORE:   PUSH   BX          ; Xfer BX to DX for bit check.
        POP    DX

        AND    DX, 0001    ; Is lowest bit 1?
        JZ     NOADD       ; No, do not add.
        ADD    CX,AX

NOADD:  SHL    AX,1        ; Shift AX left (double).
        SHR    BX,1        ; Shift BX right (integer halve, next bit).
        JNZ    MORE        ; Keep going until no more bits in BX.

FIN:    PUSH   CX          ; Xfer product from CX to AX.
        POP    AX

        POP    DX          ; Restore registers and return.
        POP    CX
        POP    BX
        RET

它依赖于123乘以456的事实与:

    123 x 6
+  1230 x 5
+ 12300 x 4

这与你在小学/小学教授乘法的方式相同。 使用二进制文件更容易,因为您只需要乘以零或一(换句话说,添加或不添加)。

这是非常古老的学校x86(8086,来自DEBUG会议 - 我不敢相信他们实际上仍然在XP中包含那个东西),因为这是我最后一次直接在汇编程序中编码。 高级语言有一些东西可以说:-)

在汇编指令中,使用时钟周期测量执行任何指令的速度。 Mul指令总是花费更多的时钟周期然后添加操作,但是如果在循环中执行相同的add指令,则使用add指令进行乘法的整个时钟周期将比单mul指令更多。 您可以查看以下URL,其中讨论了单个add / mul指令的时钟周期。因此,您可以进行数学运算,哪一个会更快。

http://home.comcast.net/~fbui/intel_a.html#add

http://home.comcast.net/~fbui/intel_m.html#mul

我的建议是使用mul指令而不是添加循环,后者是非常低效的解决方案。

我必须回应你已经做出的反应 - 对于一般的倍增你最好使用MUL - 毕竟它就是它的用途!

在某些特定情况下,您知道每次都希望乘以特定的固定值(例如,在位图中计算出像素索引),那么您可以考虑将乘法数减少到(小)一小部分SHL和ADD - 例如:

1280 x 1024显示屏 - 显示屏上的每一行为1280像素。

1280 = 1024 + 256 = 2 ^ 10 + 2 ^ 8

y * 1280 = y *(2 ^ 10)+ y *(2 ^ 8)= ADD(SHL y,10),(SHL y,8)

...鉴于图形处理可能需要快速,这种方法可以节省宝贵的时钟周期。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM