我有以下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
寄存器用于存储返回值。但是该proc
过程没有返回值。因此,我很好奇为什么在这里 %r9
使用该寄存器,而不是使用某些不用于返回值的寄存器。
我对这条指令相对于其他指令的位置也感到困惑。它首先出现,很早就%rax
需要它的目标寄存器了(实际上,直到最后一步才需要此寄存器)。它也出现在之前addq %rdi, (%rsi)
,它是过程(*a1p += a1;
)中第一行代码的翻译。
我想念什么?
它只是使用临时寄存器加载堆栈arg。 RAX是暂存注册表的首选。该函数没有返回值,因此RAX并不特殊。
通常,提前计划负载是隐藏负载使用延迟的好主意,因此,无序的exec不必费劲地隐藏它。请记住,这是优化的代码,因此每个C语句的指令都不是单独的单个块。对于一些这个简单的,这是很好的(未优化将一切存储到堆栈中,然后重新装入。另请参见本)
R9将是一个较差的选择,因为R9已被函数入口占用(带有另一个arg),从而限制了指令调度。更重要的是,因为addb %dl, (%r9)
不需要REX前缀addb %dl, (%rax)
。因此,这将浪费代码大小。
已经在使用的缺点不适用于R10或R11(像RAX一样,它们只是调用对象,但不用于arg传递),但是代码大小的缺点仍然适用。
R9B甚至没有任何意义。堆栈arg是一个指针。char a4
加载到EDX之后,唯一使用的字节寄存器是DL()。
(双字加载避免编写部分寄存器,并且不需要movzx / movzbl,因为调用者通常会写整个qword或至少是dword,即使对于窄args也是如此)。
编译器也可以早些移动此负载,但选择不移动。但是add %dl, (%rax)
RMW处于上(%rax)
,因此在dl
加载之前(%rax)
已准备好数据之前,不需要数据。尽早准备好RAX地址比DL数据更有价值,因为该地址正用于其他加载而不是ALU->存储。