在/O2
(发布)模式下查看由Visual Studio(2015U2)生成的程序集时,我看到这个“手动优化”的C代码被转换回乘法:
int64_t calc(int64_t a) {
return (a << 6) + (a << 16) - a;
}
大会:
imul rdx,qword ptr [a],1003Fh
所以我想知道这是否真的比按照它的编写方式更快,如:
mov rbx,qword ptr [a]
mov rax,rbx
shl rax,6
mov rcx,rbx
shl rcx,10h
add rax,rcx
sub rax,rbx
我一直认为乘法总是慢于几个班次/加法?现代英特尔x86_64处理器不再是这种情况吗?
答案 0 :(得分:11)
没错,现代的x86 CPU(尤其是Intel)具有非常高的性能乘数
imul r, r/m
和imul r, r/m, imm
都是3周期延迟,Intel SnB系列和AMD Ryzen每1c吞吐量一个,即使是64位操作数也是如此。
在AMD Bulldozer系列上,它具有4c或6c延迟,每2c一个或每4c吞吐一个。 (64位操作数大小的较慢时间)。
来自Agner Fog's instruction tables的数据。另请参阅x86标记wiki中的其他内容。
现代CPU中的晶体管预算非常庞大,并且允许以这种低延迟进行64位乘法所需的硬件并行度。 (It takes a lot of adders制作large fast multiplier)。
受功率预算的限制,而不是晶体管预算,意味着可以使用专用硬件来实现许多不同的功能,只要它们不能同时切换(https://en.wikipedia.org/wiki/Dark_silicon)。例如你不能在英特尔CPU上同时使pext
/ pdep
单位,整数乘数和向量FMA单位饱和,因为它们中的许多都在相同的执行端口上。
有趣的事实:imul r64
也是3c,所以你可以得到一个完整的64 * 64 =&gt; 128b乘以3个周期。 imul r32
是4c延迟和额外的uop。我的猜测是,额外的uop / cycle将64位结果从常规64位乘法器分成两个32位半。
编译器通常会针对延迟进行优化,并且通常不知道如何优化短的独立依赖关系链以实现吞吐量,而不是长循环传输的依赖关系链,这会阻碍延迟。
gcc和clang3.8及更高版本最多使用两条LEA
条指令代替imul r, r/m, imm
。我认为,如果替代方案是3条或更多条指令(不包括imul
),gcc将使用mov
。
这是一个合理的调整选择,因为3指令dep链的长度与Intel上imul
的长度相同。使用两个1周期指令花费额外的uop来将延迟缩短1个周期。
clang3.7及更早版本倾向于使用imul
。所以clang最近改为优化延迟而不是通过小常数乘以吞吐量。 (或者可能出于其他原因,比如不与其他与竞争对手只在同一端口的东西竞争。)
e.g。 this code on the Godbolt compiler explorer:
int foo (int a) { return a * 63; }
# gcc 6.1 -O3 -march=haswell (and clang actually does the same here)
mov eax, edi # tmp91, a
sal eax, 6 # tmp91,
sub eax, edi # tmp92, a
ret
clang3.8及更高版本生成相同的代码。