为什么 GCC 选择 dword movl 将长移位计数复制到 CL?

Why does GCC chose dword movl to copy a long shift count to CL?

Computer System: A Programmer's Prespective第三章中讲到移位操作时给出了示例程序:

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 <<= 4
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

如果可能,编译器更喜欢 32 位寄存器而不是 64 位寄存器,因为使用 64 位寄存器需要额外的 'REX' 前缀字节。

同理选择rsi\esi寄存器的最低位字节,32位编码没有,需要前缀。正如 Peter Cordes 评论的那样,编译器通常会避免使用 8 位寄存器,因为称为 的时间惩罚,在 CPU 如何检测依赖链的内部,out-of-order 执行和重命名寄存器。

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

  • 避免写入部分寄存器(即 8 或 16 位,其中写入合并到旧值而不是 zero-extending)。 - 由于不同微体系结构的各种原因,包括在这种情况下对 RCX 旧值的错误依赖。
  • 因为它是 x86-64 机器代码中的默认值,不需要任何前缀。对于任何指令,它至少与任何其他 operand-size 一样快。

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

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

  • movq %rsi, %rcx - 浪费 REX 前缀(code-size 缺点)。
  • movb %sil, %cl - 写入部分寄存器,仍然需要 REX 前缀才能访问 SIL。
  • movzbl %sil, %ecx - 代码大小:2 字节操作码,需要 REX 才能读取 SIL。此外,AMD CPU 只为 movl / movq 执行 mov-elimination(零延迟),而不是 movzx。
  • movw %si, %cx - 零优势,需要 operand-size 前缀并写入部分寄存器。
  • movzwl %si, %ecx - 在 code-size 上与 movq 并列,但即使在 Intel CPU 上也击败 mov-elimination。

有趣的事实:如果我们用虚拟 arg 填充,所以 n 到达 RDX,GCC 仍然选择 movl %edx, %ecx,即使 movb %dl, %cl 是相同的 code-size(无需 REX 即可访问 DL)。所以是的,GCC 肯定会避免字节 operand-size.

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

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