我知道添加与 mul 功能相比更快。
我想知道如何在以下代码中依次使用 添加 而不是 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)
答案 0 :(得分:12)
添加比 mul 更快,但如果你想将两个一般值相乘, mul 比任何循环迭代添加操作。
您无法认真使用添加来使代码比 mul 更快。如果你需要乘以一些小的常数值(例如2),那么也许你可以使用 add 来加快速度。但对于一般情况 - 没有。
答案 1 :(得分:9)
如果要将两个您事先不知道的值相乘,实际上不可能超过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
这个问题的关键问题是假设它需要至少4个时钟才能完成 由寄存器依赖性约束的超标量CPU。乘以通常需要 现代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,x和y计算t = 2 ^ k * x + y的k = 1,2,3(参见英特尔参考手册) 任何注册。如果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
弄清楚这些序列有点棘手,但你可以设置有组织的攻击。</ p>
由于LEA,SHL,SUB,NEG,MOV都是最差的单时钟指令 case和零时钟如果它们不依赖于其他指令,你可以计算任何这样的序列的执行成本。这意味着您可以实现动态编程算法,以生成此类指令的最佳序列。 这仅在时钟计数小于特定CPU的整数乘法时才有用 (我使用5个时钟作为经验法则),和它不会耗尽所有寄存器,或者 至少它不会使用已经忙的寄存器(避免任何溢出)。
我实际上将它构建到我们的PARLANSE编译器中,它非常有效地计算结构A [i]的数组的偏移量,其中A中结构元素的大小是已知常量。一个聪明的人可能会缓存答案,所以它不会 每次乘以相同的常数时必须重新计算;我实际上并没有这样做,因为 生成此类序列的时间比您预期的要少。
打印出与所有常数相乘所需的指令序列非常有趣 从1到10000.大多数可以在最坏情况下的5-6指令中完成。 因此,即使是最糟糕的索引,PARLANSE编译器也几乎不会使用实际的乘法 嵌套结构数组。
答案 2 :(得分:4)
除非你的乘法相当简单,否则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
我通常建议您编写代码主要是为了提高可读性,并且只在需要时担心性能。但是,如果您在汇编程序中工作,那么您可能已经 at 。但是我不确定我的“解决方案”是否真的适用于你的情况,因为你有一个任意的被乘数。
汇编程序根本不会改变优化的那个方面。
如果你真的想看一些更通用的汇编程序来使用add
进行乘法运算,这里的例程将在ax
和bx
中取两个无符号值并返回产品在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中包含那个东西),因为这是我最后一次直接在汇编程序中编码。对于高级语言,有一些东西可以说: - )
答案 3 :(得分:3)
当涉及汇编指令时,使用时钟周期测量执行任何指令的速度。 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指令而不是添加循环,后者是非常低效的解决方案。
答案 4 :(得分:0)
我必须回应你已经做出的反应 - 对于一般的乘法你最好使用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)
...鉴于图形处理可能需要快速,这样的方法可以为您节省宝贵的时钟周期。