为什么在程序集中使用 %rax 寄存器来处理这个带有 8 个参数的过程?

Why is the %rax register used in the assembly for this procedure with 8 args?

我有以下 C 函数:

void proc(long  a1, long  *a1p,
          int   a2, int   *a2p,
          short a3, short *a3p,
          char  a4, char  *a4p)
{
    *a1p += a1;
    *a2p += a2;
    *a3p += a3;
    *a4p += a4;
}

使用 Godbolt,我将其转换为 x86_64 程序集(为简单起见,我使用 -Og 标志来最小化优化)。它产生以下程序集:

proc:
        movq    16(%rsp), %rax
        addq    %rdi, (%rsi)
        addl    %edx, (%rcx)
        addw    %r8w, (%r9)
        movl    8(%rsp), %edx
        addb    %dl, (%rax)
        ret

我对第一行汇编感到困惑:movq 16(%rsp), %rax。我知道 %rax 寄存器用于存储 return 值。但是 proc 过程没有 return 值。所以我很好奇为什么这里使用该寄存器,而不是 %r9 或其他一些不用于 returning 值的寄存器。

我也对这条指令相对于其他指令的位置感到困惑。它首先出现,远远早于任何事情都需要它的目标寄存器 %rax 之前(实际上,直到最后一步才需要这个寄存器)。它也出现在addq %rdi, (%rsi)之前,这是程序中第一行代码的翻译(*a1p += a1;)。

我错过了什么?

它只是使用暂存 reg 来加载堆栈 arg。RAX 是暂存 reg 的首选。此函数没有 return 值,因此 RAX 并不特殊。

提前安排加载通常是隐藏加载使用延迟的好主意,因此乱序执行程序不必费力地隐藏它。请记住,这是 优化的 代码,因此每个 C 语句的指令不是单独的单个块。对于这么简单的事情,这很好(未优化会 store everything to the stack and then reload it. See also

R9 将是一个更糟糕的选择,因为它已经在函数入口处被占用(与另一个 arg),限制了指令调度。更重要的是,因为 addb %dl, (%r9) 需要 REX 前缀,而 addb %dl, (%rax) 不需要。所以它会浪费代码大小。

已经在使用的缺点不适用于 R10 或 R11(像 RAX,它们是纯粹的调用破坏但不用于 arg 传递),但代码大小的缺点仍然存在。

R9B 甚至没有意义;堆栈 arg 是一个指针。加载到 EDX 后,唯一使用的字节寄存器是 DL (char a4)。

(双字加载避免了写入部分寄存器,并且不需要 movzx / movzbl,因为调用者通常会写入整个 qword,或者至少是双字,即使对于窄参数也是如此)。

编译器也可以更早地移动此负载,但选择不这样做。但是 add %dl, (%rax)(%rax) 上的 RMW,因此在从 (%rax) 加载该数据之前不需要 dl 数据。尽早准备好 RAX 地址 比 DL 数据更有价值,因为该地址正在用于另一个加载,而不是 ALU -> 存储。