C++ 为什么除法3需要在x86上右移(和其他奇怪的事情)?
我有以下C/C++函数:C++ 为什么除法3需要在x86上右移(和其他奇怪的事情)?,c++,assembly,compilation,x86-64,integer-division,C++,Assembly,Compilation,X86 64,Integer Division,我有以下C/C++函数: unsigned div3(unsigned x){ 返回x/3; } 在-O3,这将导致: div3(unsigned int): mov ecx, edi # tmp = x mov eax, 2863311531 # result = 3^-1 imul rax, rcx # result *= tmp shr rax, 33
unsigned div3(unsigned x){
返回x/3;
}
在-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
相乘吗eax
和ecx
相乘不是更快吗imul
而不是mul
?我以为模运算都是无符号的//与3的倒数相乘:
15 * 2863311531 = 42949672965
42949672965模2^32=5
//使用定点乘法
15 * 2863311531 = 42949672965
42949672965 >> 33 = 5
//简单除以3
15 / 3 = 5
所以乘以42949672965实际上等于除以3。我假设clang的优化是基于模运算的,而实际上是基于定点运算的
编辑2
我现在意识到乘法逆只能用于没有余数的除法。例如,1乘以3-1等于3-1,而不是零。只有定点算法具有正确的舍入
不幸的是,clang没有使用模块化算法,在这种情况下,模块化算法只是一条imul
指令,即使它可以。下面的函数具有与上面相同的编译输出
unsigned div3(unsigned x){
__内建假设(x%3==0);
返回x/3;
}
(关于适用于所有可能输入的精确除法的定点乘法逆的规范问答:-不完全重复,因为它只涉及数学,而不涉及寄存器宽度和imul与mul等一些实现细节。) 最后33位右移是怎么回事?我想我们可以去掉最高的32位 与
3^(-1)mod 3
不同,您必须更多地考虑0.3333333
,
之前的0
位于上32位,而3333
位于下32位。
此定点操作工作正常,但结果明显移动到rax
的上部,因此CPU必须在操作后再次向下移动结果
为什么我们使用imul而不是mul?我以为模运算都是无符号的
没有与IMUL
指令等效的MUL
指令。所使用的IMUL
变体采用两个寄存器:
a <= a * b
a
我们不能直接用edi乘以rax吗
我们不能imulrax,rdi
,因为调用约定允许调用方在rdi的高位留下垃圾;只有EDI部分包含该值。这在内联时不是问题;写入32位寄存器不会隐式地将零扩展到完整的64位寄存器,因此编译器通常不需要额外的指令来对32位值进行零扩展
(如果无法避免,零扩展到不同的寄存器会更好)
更直截了当地说,x86没有任何乘法指令可以对其输入之一进行零扩展,从而使32位和64位寄存器相乘。两个输入的宽度必须相同
为什么我们要在64位模式下乘法
(术语:所有这些代码都在64位模式下运行。您会问为什么64位操作数大小。)
您可以mul-edi
将EAX与edi相乘,以获得在EDX:EAX上拆分的64位结果,但是mul-edi
在Intel CPU上是3个UOP,而大多数现代x86-64 CPU具有快速64位imul
。(尽管imul r64,但在AMD推土机系列和一些低功耗CPU上,r64速度较慢。)和(指令表和Microach PDF)
(有趣的事实:mul-rdi
在英特尔CPU上实际上更便宜,只有2个UOP。也许是因为不必对整数乘法单元的输出进行额外的拆分,比如mul-edi
就必须将64位低半乘法器输出拆分为EDX和EAX的一半,但对于64x64=>128位mul,这种情况自然发生。)
另外,您需要的部件在EDX中,因此您需要另一个mov eax,EDX
来处理它。(同样,因为我们正在查看函数的独立定义的代码,而不是在内联到调用方之后。)
GCC 8.3和更早版本确实使用了32位mul
而不是64位imul
()。当推土机系列和旧的Silvermont CPU更为相关时,这对于-mtune=generic
来说并不疯狂,但这些CPU在过去对于较新的GCC来说更为重要,其通用调优选择反映了这一点。不幸的是,GCC还浪费了一条将EDI复制到EAX的mov
指令,使这种方式看起来更糟:/
# gcc8.3 -O3 (default -mtune=generic)
div3(unsigned int):
mov eax, edi # 1 uop, stupid wasted instruction
mov edx, -1431655765 # 1 uop (same 32-bit constant, just printed differently)
mul edx # 3 uops on Sandybridge-family
mov eax, edx # 1 uop
shr eax # 1 uop
ret
# total of 7 uops on SnB-family
只有6个UOP具有mov eax、0xAAAAAB
/mul edi
,但仍然比以下情况更糟:
# gcc9.3 -O3 (default -mtune=generic)
div3(unsigned int):
mov eax, edi # 1 uop
mov edi, 2863311531 # 1 uop
imul rax, rdi # 1 uop
shr rax, 33 # 1 uop
ret
# total 4 uops, not counting ret
不幸的是,64位0x00000000aaaaab
不能表示为32位符号扩展立即数,因此imul-rax、rcx、0xaaaaab
不可编码。这意味着0xffffffffaaaaab
为什么我们使用imul而不是mul?我以为模运算都是无符号的
它没有签名。输入的符号性仅影响结果的高半部,但imul reg,reg
不会产生高半部。只有一个操作数f
// Warning: INEXACT FOR LARGE INPUTS
// this fast approximation can just use the high half,
// so on 32-bit machines it avoids one shift instruction vs. exact division
int32_t div10(int32_t dividend)
{
int64_t invDivisor = 0x1999999A;
return (int32_t) ((invDivisor * dividend) >> 32);
}
unsigned div3_exact_only(unsigned x) {
__builtin_assume(x % 3 == 0); // or an equivalent with if() __builtin_unreachable()
return x / 3;
}
div3_exact_only:
imul eax, edi, 0xAAAAAAAB # 1 uop, 3c latency
ret
uint32_t div3_exact_only(uint32_t x) {
return x * 0xaaaaaaabU;
}
unsigned div7(unsigned x) {
return x / 7;
}
mhi = (2^(32+L) + 2^(L))/3 = 5726623062
mlo = (2^(32+L) )/3 = 5726623061
while((L > 0) && ((mhi>>1) > (mlo>>1))){
mhi = mhi>>1;
mlo = mlo>>1;
L = L-1;
}
if(mhi >= 2^32){
mhi = mhi-2^32
L = L-1;
; use 3 additional instructions for missing 2^32 bit
}
... mhi>>1 = 5726623062>>1 = 2863311531
... mlo>>1 = 5726623061>>1 = 2863311530 (mhi>>1) > (mlo>>1)
... mhi = mhi>>1 = 2863311531
... mlo = mhi>>1 = 2863311530
... L = L-1 = 1
... the next loop exits since now (mhi>>1) == (mlo>>1)
L = ceil(log2(7)) = 3
mhi = (2^(32+L) + 2^(L))/7 = 4908534053
mhi = mhi-2^32 = 613566757
L = L-1 = 2
... visual studio generated code for div7, input is rcx
mov eax, 613566757
mul ecx
sub ecx, edx ; handle 2^32 bit
shr ecx, 1 ; ...
lea eax, DWORD PTR [edx+ecx] ; ...
shr eax, 2
mhi and L are generated based on divisor during compile time
...
quotient = (x*mhi)>>(32+L)
product = quotient*divisor
remainder = x - product