Assembly 汇编8086-在没有MUL和DIV指令的情况下实现任何乘法和除法

Assembly 汇编8086-在没有MUL和DIV指令的情况下实现任何乘法和除法,assembly,cpu-usage,division,multiplication,Assembly,Cpu Usage,Division,Multiplication,我想知道是否有一种方法可以在不使用MUL或DIV指令的情况下执行任何乘法或除法,因为它们需要大量的CPU周期。我可以利用此目标的SHL或SHR指令吗?如何实现汇编代码?实现乘法更容易,如果您还记得的话,shl操作执行的操作与指定操作数乘以2的操作相同。左移两位位置将操作数乘以四。向左移动三位位置将操作数乘以八。通常,将操作数左移n位会使其乘以2n。任何值都可以通过一系列移位和加法或移位和减法乘以某个常数。例如,要将ax寄存器乘以10,只需将其乘以8,然后将原始值的两倍相加即可。也就是说,10*a

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

实现乘法更容易,如果您还记得的话,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
但是,如果您看一下计时,上面的shift和add示例在80x86系列的大多数处理器上需要的时钟周期比mul指令少。当然,代码要大一些(几个字节),但性能改进通常是值得的。当然,在后来的80x86处理器上,mul指令比早期的处理器快很多,但在这些处理器上,移位和加法方案通常也更快

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

            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)。在使用移位、加法和减法执行乘法运算时,请注意这个陷阱


分工有点难,需要思考

像SHL/SHR、SAL/SAR、ADD/SUB这样的东西比MUL和DIV更快,但MUL和DIV对动态数字的效果更好。例如,如果你知道你只需要除以2,那么它是一个单位右移。但是,如果您事先不知道该数字,那么您可能会尝试重复细分这些值。例如,要确定AX除以BX,您可以不断地从AX中减去BX,直到BX大于AX,从而跟踪计数。但是如果你除以200,除以1意味着200个循环和子操作


MUL和DIV在大多数情况下都能更好地工作,因为所涉及的数字不是硬编码的,也不是预先知道的。我能想到的唯一例外是,当你知道它是一个类似于乘法/除以2、4、8等的函数时,移位运算符可以很好地工作。

就像汇编中的所有其他函数一样,乘法和除法有很多方法

  • 按价值进行除法
  • 使用移位和加法/减法代替乘法
  • 使用
    lea
    (仅限乘法)的地址计算选项 打破神话

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

    MUL
    IMUL
    在现代CPU上速度极快,请参见:
    DIV
    IDIV
    一直都非常慢

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

    MUL,IMUL r64:延迟3个周期,反向吞吐量1个周期

    请注意,这是将两个64相乘的最大延迟!位值。
    如果CPU所做的只是乘法,那么它可以在每个CPU周期完成其中一个乘法。
    如果你认为使用移位和增加乘以7的上述示例具有4个周期的延迟(3使用LEA)。在现代CPU上,没有真正的方法可以打败普通乘法运算

    倒数乘

    根据:

    在大多数微处理器上,除法运算速度很慢。浮点运算 计算时,我们可以用相同的除数进行多个除法 与倒数相乘会更快,例如:

    float a, b, d;  
    a /= d; b /= d;   
    
    可更改为:

    float a, b, d, r;   
    r = 1.0f / d;   
    a *= r; b *= r;   
    
    如果我们想对整数做类似的事情,那么我们必须将倒数除数缩放2n,然后将n个位置移动到 乘法后的右键

    当需要除以常数或连续多次除以同一变量时,乘以倒数效果很好。
    您可以在中找到演示此概念的非常酷的汇编代码

    移动和添加/subs
    右移是一个除以2的过程。
    shr
    -(Reduce)。
    左移是2乘以shl-(Larger)。
    您可以添加和减法,以修正过程中的非二次幂

    //Multiply by 7
    mov ecx,eax
    shl eax,3    //*8
    sub eax,ecx  //*7
    
    使用此方法除2的幂之外的除法很快变得复杂。
    您可能想知道为什么我会以一种奇怪的顺序执行这些操作,但我正试图使操作尽可能简短,以最大限度地增加可并行执行的指令数

    使用
    Lea

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

    但是请注意,带有乘法器(比例因子)的
    lea
    被认为是AMD CPU上从K10到Zen的“复杂”指令,其延迟为2个CPU周期。在早期的AMD CPU(k8)上,
    lea
    始终具有2个周期的延迟,即使使用简单的
    [reg+reg]
    o
                          //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
    
    mov bx, 1000b
    shl bx, 5
    mov cx, bx
    shr cx, 2
    add bx, cx
    add bx, 1000b