如果您将两个事先不知道的值相乘,则实际上不可能击败 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=2^k*x+y for k=1,2,3(参见 Intel 参考手册),其中 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 编译器在索引最讨厌的嵌套结构数组时几乎从不使用实际的乘法。