C 高效汇编乘法
不久前开始练习组装。 我想通过汇编命令lea和shift实现有效的乘法。 我想写一个c程序,它将调用一个装配过程,该装配过程符合用户接收到的常数参数,并将用户接收到的另一个参数乘以该常数 如何使此代码有效?C 高效汇编乘法,c,assembly,x86,nasm,micro-optimization,C,Assembly,X86,Nasm,Micro Optimization,不久前开始练习组装。 我想通过汇编命令lea和shift实现有效的乘法。 我想写一个c程序,它将调用一个装配过程,该装配过程符合用户接收到的常数参数,并将用户接收到的另一个参数乘以该常数 如何使此代码有效? 我可以将哪些数字分组(如果有)以符合相同的程序? 例如,我认为我可以分组2,4,8,。。。与左移1,2,3的步骤相同 但是我很难找到像这样有其他数字的其他组,那么负数呢…本练习有趣的部分是找到使用1或2 LEA、SHL和/或ADD/SUB指令实现各种常量乘法的方法 实际上,为一次乘法而动态调
我可以将哪些数字分组(如果有)以符合相同的程序? 例如,我认为我可以分组2,4,8,。。。与左移1,2,3的步骤相同
但是我很难找到像这样有其他数字的其他组,那么负数呢…本练习有趣的部分是找到使用1或2 LEA、SHL和/或ADD/SUB指令实现各种常量乘法的方法 实际上,为一次乘法而动态调度并不十分有趣,这意味着要么是实际的JIT编译,要么是在一个巨大的代码块表中已经存在了所有可能的序列。(如
开关语句。)
相反,我建议编写一个C或Python或任何接受1个整数arg的函数,并作为输出生成asm源文本,该文本实现x*n
,其中n
是整数arg。即,类似于您在编译器中找到的优化乘常数的函数
您可能需要制定一种自动化的方法来测试这一点,例如,通过比较两个不同的x
值与纯Cx*n
进行比较
如果你不能在2条指令中完成任务(或者3条指令中有一条指令是mov
),那就不值得了。现代x86的硬件乘法效率高得离谱imul reg、r/m、imm
为1 uop、3周期延迟、完全管道化。(AMD从Zen开始,Intel从Core2或Nehalem开始等等。)对于关键路径长度为1或2个周期无法完成的任何事情,这都是您的退路(如果您愿意,假设零延迟mov,如IvyBridge+和Zen。)
或者,如果您想探索更复杂的序列,可以在回退之前设置更高的阈值,例如,在推土机系列上实现64位乘法(6个周期延迟)。甚至是P5奔腾,其中imul
需要9个周期(不可配对)
要寻找的模式
整数乘法归结为将1个操作数的移位副本相加,其中另一个操作数具有1
位。(请参阅执行乘法运行时变量值、移位和加法检查每一位的算法。)
当然,最简单的模式只有一个设置位,即2的幂;那就左移吧。这很容易检查:n&(n-1)==0
,当n!=0
任何正好有2个设置位的操作最多只能进行2次移位和一次加法。(GNU C\uuu内置\uu popcount(n)
对设置位进行计数。在x86 asm中,SSE4.2popcnt
)
GNU C内置ctz
查找最低设置位的位索引。在您知道的非零数字上使用它将获得低位的移位计数。在x86 asm中,bsf
/tzcnt
要清除最低设置位并“暴露”下一个最低设置位,可以执行n&=n-1代码>。在x86 asm或LEA/AND中
另一个有趣的模式是2n+-1。+1情况已经被2位设置覆盖,但低位的移位计数为0;不需要换班。轮班次数最多为3次时,您可以在一个LEA内完成
通过检查n+1
是否为2的幂(仅设置了1位),可以检测2^n-1。更复杂的是,(2^n-1)*2^m
可以通过这个技巧加上另一个移位来完成。因此,您可以尝试右移,将最低设置位移到底部,然后寻找技巧
GCC以2^n-1的方式执行此操作:
mul15: # gcc -O3 -mtune=bdver2
mov eax, edi
sal eax, 4
sub eax, edi
ret
clang效率更高(对于缩放索引仍然只有1个周期延迟的Intel CPU):
结合这些模式
也许可以将你的数字分解成它的主要因子,并寻找方法使用你的构建块来组合这些因子
但这不是唯一的方法。您可以按x*11
的方式执行x*5*2+x
,就像GCC和Clang这样做(非常类似)
对于x*17也有两种方法。GCC和Clang这样做:
mul17:
mov eax, edi
sal eax, 4
add eax, edi
ret
但是,即使使用-march=sandybridge
(无mov消除,1周期LEA[reg+reg*scale]
)他们也无法使用的另一种方法是:
因此,我们不是将因子相乘,而是将不同的乘数相加,形成总乘数
除了简单的2位或2^n+-1之外,我没有任何关于如何以编程方式搜索这些序列的好建议。如果您感到好奇,可以在GCC或LLVM源代码中查找执行这些优化的函数;他们发现了很多棘手的问题
这项工作可能分为目标中性优化过程和x86特定目标代码的幂2优化过程,用于使用LEA,以及在返回到imul
-immediate之前决定多少指令值得的阈值
负数
x*-8
可以使用x-x*9
完成。我认为即使x*9
溢出,这可能是安全的,但您必须仔细检查
查看编译器输出
我将其用于x86-64系统V ABI(RDI中的第一个参数,如上面的示例)。用gcc和铿锵-O3。我使用了-mtune=bdver2
(Piledriver),因为它的乘法速度比Intel或Zen稍慢。这鼓励GCC和Clang稍微更积极地避免imul
我没有尝试如果long
/uint64\u t
会改变这一点(6个周期而不是4个周期的延迟,吞吐量的一半),或者如果旧的uarch lik
lea eax, [rdi + 4*rdi]
lea eax, [rdi + 2*rax]
mul17:
mov eax, edi
sal eax, 4
add eax, edi
ret
mul17:
lea eax, [rdi + 8*rdi] ; x*9
lea eax, [rax + 8*rdi] ; x*9 + x*8 = x*17
#define MULFUN(c) int mul##c(int x) { return x*c; }
MULFUN(9)
MULFUN(10)
MULFUN(11)
MULFUN(12)
...