首页 > 解决方案 > 为什么 GCC 选择 dword movl 将长移位计数复制到 CL?

问题描述

在《计算机系统:程序员的视角》第三章中,在谈到移位操作时,给出了一个示例程序:

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

它的汇编代码如下(可在 Godbolt 编译器资源管理器上使用 x86-64 的 GCC10.2重现-O1-O2以不同的顺序安排指令,但仍用于movlECX):

shift_left4_rightn:
  endbr64
  movq %rdi, %rax     Get x
  salq $4, %rax       x <<= 4
  movl %esi, %ecx     Get n
  sarq %cl, %rax      x >>= n
  ret

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

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

结果,在处理长整数时,我真的无法弄清楚使用“ movl %esi, %ecx ”的原因。

标签: assemblygccx86-64micro-optimization

解决方案


是的,GCC 意识到高位被sar.
然后movl是应用两个简单优化规则的自然结果:

  • 避免写入部分寄存器(即 8 位或 16 位,其中写入合并到旧值而不是零扩展)。 为什么 GCC 不使用部分寄存器?- 由于不同微架构的各种原因,包括在这种情况下对 RCX 旧值的错误依赖。
  • 首选 32 位操作数大小,因为它是 x86-64 机器代码中的默认值,不需要任何前缀。对于任何指令,它至少与任何其他操作数大小一样快。

有趣的事实:即使 arg 是uint8_t,编译仍然希望使用movl %esi, %ecx. 当 arg 值仅在 SIL 中时,您会认为读取更宽的寄存器可能会造成部分寄存器停顿,但 x86-64 System V 调用约定的非官方扩展是调用者应将窄 args 扩展为零或符号至少32 位。所以我们可以假设它是用至少 32 位操作编写的。

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

  • movq %rsi, %rcx- 浪费一个 REX 前缀(代码大小的缺点)。
  • movb %sil, %cl- 写入部分寄存器,但仍需要 REX 前缀才能访问 SIL。
  • movzbl %sil, %ecx- 代码大小:2 字节操作码,需要 REX 才能读取 SIL。此外,AMD CPU 仅对movl/进行 mov-elimination(零延迟) movq,而不是 movzx。
  • movw %si, %cx- 零优势,需要操作数大小的前缀并写入部分寄存器。
  • movzwl %si, %ecx- 与movq代码大小相关,但即使在英特尔 CPU 上也无法消除 mov。

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

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

但如果我们计算 arg unsigned char,clang 和 GCC 都使用movl而不是movb,幸运的是。 https://godbolt.org/z/e95WP8


推荐阅读