为什么GCC选择了dword movl将很长的班次计数复制到CL?

问题描述

计算机系统:程序员的视线的第三章中,在讨论移位操作时给出了一个示例程序:

long shift_left4_rightn(long x,long n)
{
    x <<= 4;
    x >>= n;
    return x;
}

其汇编代码如下(可复制GCC10.2 -O1 for x86-64 on the Godbolt compiler explorer-O2以不同的顺序安排指令,但仍将movl用于ECX):

shift_left4_rightn
endbr64
movq%rdi,%rax 获取x
salq 4美元,%rax x
movl%esi,%ecx 获取n
sarq%cl,%rax x >> = n
ret

我想知道为什么获取n的汇编代码是movl %esi,%ecx而不是movq %rsi,%rcx,因为n是一个四字。

另一方面,如果考虑优化,movb %sil,%cl可能更合适,因为移位量仅使用单字节寄存器元素%cl,而那些较高的位都将被忽略。

结果,我真的不知道在处理长整数时使用“ movl%esi,%ecx ”的原因。

解决方法

是的,GCC意识到sar将忽略高位。
那么movl是应用两个简单的优化规则的自然结果:

  • 避免写入部分寄存器(即8或16位,其中写入会合并到旧值而不是零扩展中)。 Why doesn't GCC use partial registers?-由于不同的微体系结构,出于各种原因,包括在这种情况下对RCX的旧值的错误依赖。
  • Prefer 32-bit operand size,因为它是x86-64机器代码中的默认值,不需要任何前缀。而且对于任何指令,它至少与其他任何操作数大小一样快。

有趣的事实:即使arg是uint8_t,编译程序还是希望使用movl %esi,%ecx。您可能会认为,当arg值仅在SIL中时,读取更宽的寄存器可能会导致部分寄存器停顿,但是对x86-64 System V调用约定的非官方扩展是callers should zero or sign extend narrow args to at least 32-bit。因此,我们可以假定它是使用至少32位操作编写的。

其他一些选择的具体缺点:

  • movq %rsi,%rcx-浪费REX前缀(代码大小不足)。
  • movb %sil,%cl-写入部分寄存器,但仍需要REX前缀才能访问SIL。
  • movzbl %sil,%ecx-代码大小:2字节操作码,需要REX才能读取SIL。另外,AMD CPU仅对movl / movq进行消除运动(零延迟),而不是movzx。
  • movw %si,%cx-零优势,需要操作数大小的前缀并写入部分寄存器。
  • movzwl %si,%ecx-与movq捆绑在一起以获取代码大小,但即使在Intel CPU上也无法消除移动消除。

有趣的事实:如果我们使用虚拟参数填充,使得n到达RDX,即使movl %edx,%ecx的代码大小相同,GCC仍会选择movb %dl,%cl(不需要REX访问DL)。所以是的,GCC绝对避免使用字节操作数大小。

有趣的事实2:不幸的是,Clang确实浪费了movq上的REX,却错过了此优化。 https://godbolt.org/z/6GWhMd

但是,如果我们将计数设为arg unsigned char,那么幸运的是clang和GCC都使用movl而不是movbhttps://godbolt.org/z/e95WP8

,

在可能的情况下,编译器更喜欢32位寄存器而不是64位寄存器,因为使用64位寄存器需要额外的“ REX”前缀字节。

rsi\esi寄存器的最低字节的选择同样适用,这在32位编码中不可用,因此需要前缀。正如彼得·科德斯(Peter Cordes)所评论的那样,由于称为partial register stalls的时间惩罚,编译器通常避免使用8位寄存器,这是CPU如何检测依赖链,乱序执行和重命名寄存器的内部原因。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...