为什么在程序集中使用 %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 -> 存储。
我有以下 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 -> 存储。