我想知道是否有一种方法可以在不使用MUL或DIV指令的情况下执行任何乘法或除法,因为它们需要大量的CPU周期。我可以为此目标利用SHL或SHR指令吗?如何实现汇编代码?
答案 0 :(得分:8)
就像汇编中的其他所有东西一样,有许多方法可以进行乘法和除法。
lea
的地址计算选项(仅限乘法)。 神话破坏
现代CPU上的因为它们需要大量的CPU周期
MUL
和IMUL
速度非常快,请参阅:http://www.agner.org/optimize/instruction_tables.pdf
DIV
和IDIV
一直都非常慢。
英特尔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)。使用移位,加法和减法来执行乘法运算时要小心这个缺陷。
分部有点难,需要思考......