【发布时间】:2020-12-04 15:21:20
【问题描述】:
我有以下 C/C++ 函数:
unsigned div3(unsigned x) {
return x / 3;
}
When compiled using clang 10-O3,这会导致:
div3(unsigned int):
mov ecx, edi # tmp = x
mov eax, 2863311531 # result = 3^-1
imul rax, rcx # result *= tmp
shr rax, 33 # result >>= 33
ret
我的理解是:除以 3 相当于乘以乘法逆 3-1 mod 232 即 2863311531。
虽然有些东西我不明白:
- 为什么我们需要使用
ecx/rcx?我们不能直接将rax与edi相乘吗? - 为什么要在 64 位模式下进行乘法运算?将
eax和ecx相乘不是更快吗? - 为什么我们使用
imul而不是mul?我以为模算术都是无符号的。 - 最后的 33 位右移是怎么回事?我认为我们可以只删除最高的 32 位。
编辑 1
对于那些不明白我所说的 3-1 mod 232 的人,我在这里谈论的是乘法逆。 例如:
// multiplying with inverse of 3:
15 * 2863311531 = 42949672965
42949672965 mod 2^32 = 5
// using fixed-point multiplication
15 * 2863311531 = 42949672965
42949672965 >> 33 = 5
// simply dividing by 3
15 / 3 = 5
所以乘以 42949672965 实际上相当于除以 3。我假设 clang 的优化是基于模运算的,而实际上它是基于定点运算的。
编辑 2
我现在意识到乘法逆只能用于没有余数的除法。例如,将 1 乘以 3-1 等于 3-1,而不是零。只有定点算术具有正确的舍入。
不幸的是,clang 没有使用任何模运算,在这种情况下它只是一条 imul 指令,即使它可以。以下函数的编译输出与上述相同。
unsigned div3(unsigned x) {
__builtin_assume(x % 3 == 0);
return x / 3;
}
(关于适用于每个可能输入的精确除法的定点乘法逆的规范问答:Why does GCC use multiplication by a strange number in implementing integer division? - 不完全重复,因为它只涵盖数学,而不是一些实现细节,如寄存器宽度和 imul vs.数)
【问题讨论】:
-
我认为是因为 ecx 和 edx 是 32 位寄存器,而 rax 和 rcx 是 64 位寄存器。
-
您的所有问题都可以通过一个简单的语句来回答。编译器优化设计者认为这个序列是最有效和最优化的除法方式。通常,这些决策是根据一组标准做出的,例如最小化使用的指令数量,选择最短时间的指令,最小化数据移动量,尤其是在访问 RAM 时,努力优化 CPU 指令流水线的使用,以及其他速度考虑因素。
-
@J.Schultke 为了一探究竟,研究一下clang优化器的源码
-
2863311531等于3^-1 << 33。你的乘法系数太大了2^33。因此,您不能只切掉高 32 位,而必须切掉低 33 位,这是由移位完成的。 -
除法指令通常是处理器能做的最慢的事情。任何可以完成的替换通常都会赢得执行时间。如果
1/n可以在编译时计算,那么乘以1/n通常会更好,而不是除以n。如果我不能依赖编译器为我解决问题,我会自己手动完成。
标签: c++ assembly compilation x86-64 integer-division