Assembly 为什么GCC选择dword movl将长移位计数复制到CL?
在《计算机系统:程序员展望》的第三章中,在讨论移位操作时给出了一个示例程序:Assembly 为什么GCC选择dword movl将长移位计数复制到CL?,assembly,gcc,x86-64,micro-optimization,Assembly,Gcc,X86 64,Micro Optimization,在《计算机系统:程序员展望》的第三章中,在讨论移位操作时给出了一个示例程序: long shift_left4_rightn(长x,长n) { x=n; 返回x; } 其组装代码如下所示(可与.-O2以不同顺序调度指令,但仍使用movl到ECX): 左移4右移: endbr64 movq %rdi,%rax 获取x salq $4%rax x=n ret 我想知道为什么获取n的汇编代码是movl %esi,%ecx而不是movq %rsi,%rcx因为n
long shift_left4_rightn(长x,长n)
{
x=n;
返回x;
}
其组装代码如下所示(可与.-O2
以不同顺序调度指令,但仍使用movl
到ECX):
左移4右移:endbr64
movq %rdi,%rax 获取x
salq $4%rax x=n
ret 我想知道为什么获取n的汇编代码是
movl %esi,%ecx
而不是movq %rsi,%rcx
因为n
是一个四字
另一方面,movb %如果考虑优化,则sil,%cl
可能更合适,因为移位量仅使用单字节寄存器元素%cl
,并且这些较高的位都被忽略
因此,我真的无法找出使用“movl”的原因 %esi,%ecx“在处理长整数时。如果可能,编译器更喜欢32位寄存器而不是64位寄存器,因为使用64位寄存器需要额外的“REX”前缀字节
这同样适用于选择
rsi\esi
寄存器的最低字节,这在32位编码中不可用,因此需要前缀。正如Peter Cordes所评论的,编译器通常避免使用8位寄存器,这是由于调用了时间惩罚,这是CPU如何检测依赖链、无序执行和重命名寄存器的内部原因。是的,GCC意识到高位被sar
忽略然后
movl
是应用两个简单优化规则的自然结果:
- 避免写入部分寄存器(即8位或16位,写入合并到旧值中,而不是零扩展)由于不同微体系结构的各种原因,包括在本例中,对旧值RCX的错误依赖
- 因为它是x86-64机器代码中的默认值,不需要任何前缀。对于任何指令,它至少与任何其他操作数大小一样快
uint8\t
,编译器仍希望使用movl%esi,%ecx
。您可能认为,当arg值仅在SIL中时读取更宽的寄存器可能会造成部分寄存器暂停,但x86-64 SystemV调用约定的非官方扩展就是这样。所以我们可以假设它至少是用32位操作编写的
其他一些选择的具体缺点:
-浪费REX前缀(代码大小下降)movq%rsi,%rcx
-写入部分寄存器,仍然需要REX前缀才能访问silmovb%sil,%cl
-代码大小:2字节操作码,需要REX读取sil。此外,AMD CPU仅对movzbl%sil,%ecx
/movl
执行mov消除(零延迟),而不是movzxmovq
-零优势,需要操作数大小前缀并写入部分寄存器movw%si,%cx
-在代码大小方面与movzwl%si,%ecx
保持一致,但即使在Intel CPU上也无法消除movmovq
有趣的事实:如果我们用一个伪参数填充,因此
n
到达RDX,GCC仍然选择movl%edx,%ecx
,即使movb%dl,%cl
是相同的代码大小(不需要REX访问dl)。所以,是的,GCC肯定在避免字节操作数大小
有趣的事实2:Clang不幸地在movq
上浪费了一个REX,错过了这个优化
但如果我们将计数arg
unsigned char
,幸运的是,clang和GCC都使用movl
,而不是movb
非常确定GCC避免了SIL,因为它尽可能避免了部分寄存器欺骗,如果arg已经到达EDX,它将做出相同的选择。这里不需要部分寄存器暂停movb
将写入部分寄存器,但不需要读取完整寄存器。(这是在叶函数中调用clobbered reg)。因此,部分寄存器重命名实际上可以使其完全正常。问题不在于部分寄存器暂停,而在于CPU上没有将低位字节与完整寄存器分开重命名(Intel Haswell和更高版本,以及任何非Intel)。在这里,您将对完整寄存器有一个错误的依赖关系。避免这种错误的dep是Intel P6系列进行部分寄存器重命名的原因,这在这里非常有效。(但一般来说,编译器可能不会仔细分析,只要有可能,就尽量避免编写8位寄存器,即使使用-mtune=nehalem
)