程序集8086 - 在没有MUL和DIV指令的情况下实现任何乘法和除法

时间:2015-01-13 12:48:08

标签: assembly cpu-usage division multiplication

我想知道是否有一种方法可以在不使用MUL或DIV指令的情况下执行任何乘法或除法,因为它们需要大量的CPU周期。我可以为此目标利用SHL或SHR指令吗?如何实现汇编代码?

4 个答案:

答案 0 :(得分:8)

就像汇编中的其他所有东西一样,有许多方法可以进行乘法和除法。

  1. multiplying by the reciprocal值除以。
  2. 使用shift并添加/ subs而不是乘法。
  3. 使用lea的地址计算选项(仅限乘法)。
  4. 神话破坏

      

    因为它们需要大量的CPU周期

    现代CPU上的

    MULIMUL速度非常快,请参阅:http://www.agner.org/optimize/instruction_tables.pdf
    DIVIDIV一直都非常慢。

    英特尔Skylake的示例(第217页):

      

    MUL,IMUL r64:延迟3个周期,相互吞吐量1个周期。

    请注意,这是乘以两个64的最大延迟!位值。
    如果它所做的只是乘法,CPU可以在每个CPU周期完成这些乘法之一 如果你认为上面的例子使用shift并且加上乘以7有一个4个周期的延迟(3个使用lea)。在现代CPU上没有真正的方法可以击败普通的倍数。

    乘以互数

    根据Agner Fog's asm lib instruction page 12

      

    大多数微处理器的分工都很慢。在浮点   计算,我们可以使用相同的除数进行多个除法   通过乘以倒数来加快速度,例如:

    float a, b, d;  
    a /= d; b /= d;   
    
         

    可以更改为:

    float a, b, d, r;   
    r = 1.0f / d;   
    a *= r; b *= r;   
    
         

    如果我们想用整数做类似的事情那么我们必须将倒数除数除以2n然后将n位移到   在乘法后的右边。

    当你需要除以一个常数或者你连续多次除以同一个变量时,乘以倒数的效果很好。
    你可以找到真正很酷的汇编代码来展示Agner Fog's assembly library中的概念。

    移位并添加/ subs
    右移是两分shr - ( R educe) 向左移动是乘以shl - ( L arger) 您可以添加和减去以一路纠正两个非幂。

    //Multiply by 7
    mov ecx,eax
    shl eax,3    //*8
    sub eax,ecx  //*7
    

    使用此方法除了2的幂之外的分割很快就会变得复杂 您可能想知道我为什么要以奇怪的顺序执行操作,但我正在尝试使dependency chain尽可能短,以最大化并行执行的指令数。

    使用Lea
    Lea是计算地址偏移的指令 它可以在一条指令中计算2,3,4,5,8和9的倍数 像这样:

                          //Latency on AMD CPUs (K10 and later, including Jaguar and Zen)
                          //On Intel all take 1 cycle.
    lea eax,[eax+eax]     //*2     1 cycle      
    lea eax,[eax*2+eax]   //*3     2 cycles
    lea eax,[eax*4]       //*4     2 cycles   more efficient: shl eax,2 (1 cycle)
    lea eax,[eax*4+eax]   //*5     2 cycles 
    lea eax,[eax*8]       //*8     2 cycles   more efficient: shl eax,3 (1 cycle)
    lea eax,[eax*8+eax]   //*9     2 cycles
    

    但请注意,带有乘数(比例因子)的lea被认为是AMD CPU从K10到Zen的“复杂”指令,并且具有2个CPU周期的延迟。在较早的AMD CPU(k8)上,即使使用简单的lea[reg+reg]寻址模式,[reg+disp8]也始终具有2周期延迟。

    <强> AMD
    对于AMD Zen来说,Agner Fog的指令表是错误的:根据InstLatx64(http://instlatx64.atw.hu/),3组件或缩放索引LEA在Zen上仍然是2个周期(每个时钟吞吐量只有2个而不是4个)。此外,与早期的CPU一样,在64位模式下lea r32, [r64 + whatever]具有2个周期延迟。因此,在AMD CPU上使用lea rdx, [rax+rax]代替lea edx, [rax+rax]实际上更快,不像英特尔那样将结果截断为32位是免费的。

    使用shl可以更快地完成* 4和* 8,因为简单的移位只需要一个周期。

    在正面,lea不会改变标志,它允许自由移动到另一个目的地寄存器。 因为lea只能向左移0,1,2或3位(也就是乘以1,2,4或8),所以这是你得到的唯一中断。

    <强>英特尔
    在Intel CPU(Sandybridge系列)上,任何双组件LEA(仅一个+)都具有单周期延迟。因此lea edx, [rax + rax*4]具有单周期延迟,但lea edx, [rax + rax + 12]具有3个周期延迟(以及更差的吞吐量)。在C++ code for testing the Collatz conjecture faster than hand-written assembly - why?中详细讨论了这种权衡的一个例子。

答案 1 :(得分:2)

像SHL / SHR,SAL / SAR,ADD / SUB这样的东西比MUL和DIV快,但MUL和DIV对于动态数字更好。例如,如果您知道您只需要除以2,那么它就是右移一位。但是如果你事先并不知道这个数字,那么你可能会想要反复提取数值。例如,要确定AX除以BX,您可以不断地从AX中减去BX直到BX> AX,跟踪计数。但是,如果你除以200,那么将意味着200个循环和SUB操作。

MUL和DIV在大多数情况下都能更好地工作,因为所涉及的数字不是硬编码的并且事先已知。我能想到的唯一例外是,当你知道它是多重/除以2,4,8等等时,其中Shift运算符可以正常工作。

答案 2 :(得分:1)

以下是一个例子:

mov bx, 1000b
shl bx, 5
mov cx, bx
shr cx, 2
add bx, cx
add bx, 1000b

答案 3 :(得分:0)

实现乘法更容易,如果你还记得,shl操作执行与将指定的操作数乘以2相同的操作。向左移动两位位置将操作数乘以四。向左移动三位位置将操作数乘以八。通常,将操作数移位到左侧n位将其乘以2n。任何值都可以乘以一些常数,使用一系列的移位和加法或移位和减法。例如,要将ax寄存器乘以10,您只需要将它乘以8然后再加上原始值的两倍。也就是说,10 * ax = 8 * ax + 2 * ax。完成此任务的代码是

            shl     ax, 1           ;Multiply AX by two
            mov     bx, ax          ;Save 2*AX for later
            shl     ax, 1           ;Multiply AX by four
            shl     ax, 1           ;Multiply AX by eight
            add     ax, bx          ;Add in 2*AX to get 10*AX

使用shl比使用mul指令更快地将ax寄存器(或几乎任何寄存器)乘以大多数常量值。这似乎很难相信,因为它只需要两条指令来计算这个产品:

            mov     bx, 10
            mul     bx

但是,如果查看时序,上面的移位和添加示例要求80x86系列中大多数处理器的时钟周期少于mul指令。当然,代码有点大(几个字节),但性能提升通常是值得的。当然,在后来的80x86处理器上,mul指令比早期的处理器快得多,但移位和添加方案在这些处理器上通常也更快。

您还可以使用带移位的减法来执行乘法运算。考虑以下乘以7:

            mov     bx, ax          ;Save AX*1
            shl     ax, 1           ;AX := AX*2
            shl     ax, 1           ;AX := AX*4
            shl     ax, 1           ;AX := AX*8
            sub     ax, bx          ;AX := AX*7

这直接来自ax * 7 =(ax * 8)-ax。

的事实

初学汇编语言学生所犯的常见错误是减去或添加一个或两个而不是ax * 1或ax * 2。以下不计算ax * 7:

            shl     ax, 1
            shl     ax, 1
            shl     ax, 1
            sub     ax, 1

它计算(8 * ax)-1,完全不同(当然,除非ax = 1)。使用移位,加法和减法来执行乘法运算时要小心这个缺陷。

分部有点难,需要思考......