高效程序集乘法

问题描述 投票:2回答:1

不久前开始练习汇编。我想通过组装命令lea和shift实现高效的乘法。我想编写一个c程序,该程序将调用适合用户接收的常量参数的汇编过程,并将用户接收的另一个参数乘以该常量。

如何使此代码有效?我可以将哪些数字分组(如果有的话)以适合同一过程?例如,我认为我可以将2,4,8,...分组为相同的程序,例如,它们只是左移1,2,3。]

但是我很难找到像这样的其他数字和其他数字的团体,对负数呢?

c assembly x86 nasm micro-optimization
1个回答
3
投票

此练习的有趣部分是找到使用1或2条LEA,SHL和/或ADD / SUB指令来实现乘以各种常数的乘法的方法。

实际进行一次乘法的动态调度不是很有趣,这可能意味着实际的JIT编译,或者已经在庞大的微型代码块表中提供了所有可能的序列。 (例如switch语句。)

相反,我建议编写一个C或Python或任何采用1个整数arg的函数,并且作为输出生成实现x * n的asm源文本,其中n是整数arg。即一个函数就像您在优化乘常数的编译器中可能会发现的。

[您可能希望以一种自动化的方式来进行测试,例如通过与纯C x * n比较两个不同的x值。


如果您无法用2条指令(或3条指令中的某条为mov,则无法完成工作,那是不值得的]。现代的x86在硬件上具有可笑的高效乘法。 imul reg, r/m, imm是1 uop,3个周期的延迟,完全流水线。 (AMD自Zen以来为AMD,Intel自Core2或Nehalem以来为Intel。)这是您无法使用临界路径长度为1或2个周期(如果您愿意的话,假设零延迟mov,例如IvyBridge +和Zen)无法完成的所有操作的后备。)

或者,如果您想探索更复杂的序列,例如,可以在回退之前设置较高的阈值。旨在在推土机系列上实现64位乘法(6个周期的延迟)。 https://agner.org/optimize/。甚至是imul需要9个周期(不可配对)的P5奔腾。


要寻找的样式

整数乘法归结为1个操作数的移位副本,其中另一个操作数具有1位。 (请参阅用于实现乘以运行时变量值,通过移位并一次对每个位进行加法检查的算法。)

最简单的模式当然只有一个设置位,即2的幂;那只是左移这很容易检查:n & (n-1) == 0,当n != 0时。

具有正好2个设置位的任何东西最多2个移位和一个加号。 (GNU C __builtin_popcount(n)对设置的位进行计数。在x86 asm中,SSE4.2 popcnt。)>

GNU C __builtin_ctz查找最低设置位的位索引。在您知道的非零数字上使用它会为您提供低位的移位计数。

在x86 asm中,bsf / tzcnt

要清除最低设置位并“暴露”下一个最低位,您可以执行n &= n-1;。在x86 asm中,BMI1 blsr或LEA / AND。


另一个值得关注的模式是2 n

+ -1。 +1的情况已被2位的情况所涵盖,但低位的移位计数为0;低位的移位计数为0。无需换档。最多有3个班次,您可以在一个LEA中完成。

您可以通过检查blsr是否为2的幂(仅设置1位)来检测2 ^ n-1。稍微复杂一点的是,n+1可以通过此技巧加上另一个转换来完成。因此,您可以尝试右移以将最低的设置位带到底部,然后寻找技巧。

GCC以2 ^ n-1的方式执行此操作:

(2^n - 1) * 2^m

clang效率更高(对于比例索引仍然只有1个周期延迟的Intel CPU:]

mul15:              # gcc -O3 -mtune=bdver2
        mov     eax, edi
        sal     eax, 4
        sub     eax, edi
        ret

组合这些模式

也许将您的数字分解为主要因素,并寻找使用构建基块来组合这些因素的方法。

但这不是唯一的方法。您可以像mul15: # clang -O3 -mtune=bdver2 lea eax, [rdi + 4*rdi] lea eax, [rax + 2*rax] ret 一样执行x*11,就像GCC和Clang一样(这很像x*5*2 + x

How to multiply a register by 37 using only 2 consecutive leal instructions in x86?

x * 17也有2种方法。 GCC和Clang就是这样:

        lea     eax, [rdi + 4*rdi]
        lea     eax, [rdi + 2*rax]

但是他们即使在mul17: mov eax, edi sal eax, 4 add eax, edi ret 时也无法使用的另一种方法(无运动消除,一周期-march=sandybridge)是:

LEA [reg + reg*scale]

因此,我们没有添加乘数,而是添加了不同的乘数以构成总乘数。


我对如何以编程方式搜索这些序列(除了2个置位或2 ^ n +-等简单序列之外,没有什么很好的建议。如果您很好奇,请查看GCC或LLVM源代码以了解更多信息)进行这些优化的功能;发现很多棘手的问题。

[对于使用LEA的2次幂与特定于x86的目标代码,以及在决定退回到mul17: lea eax, [rdi + 8*rdi] ; x*9 lea eax, [rax + 8*rdi] ; x*9 + x*8 = x*17 之前确定多少指令值得的阈值,工作可以在目标无关的优化遍历之间进行划分。 。


负数

imul可以用x * -8完成。我认为即使x - x*9溢出也可能是安全的,但您必须仔细检查。


查看编译器输出
x*9

我为x86-64系统V ABI输入了#define MULFUN(c) int mul##c(int x) { return x*c; } MULFUN(9) MULFUN(10) MULFUN(11) MULFUN(12) ... (RDI中的第一个arg,就像上面的示例一样)。使用gcc和clang -O3。我使用on the Godbolt compiler explorer(Piledriver),因为它的乘法速度比Intel或Zen慢。这鼓励GCC和Clang更加积极地避免-mtune=bdver2

[我没有尝试imul / long是否会改变这一点(6个周期而不是4个周期的等待时间,而吞吐量只有一半。)或者如果像uint64_t(Pentium 4)这样的旧版uarch会有所作为。-mtune=noconadid

与GCC的默认-mtune=bdver2至少有所不同。

如果使用tune=generic,则可以使用甚至更老的Uarches,例如-m32(按顺序P5)。我建议为此使用-mtune=pentium,以便args仍传递到寄存器中,而不是堆栈中。

© www.soinside.com 2019 - 2024. All rights reserved.