我试图查看一个非常简单的程序的汇编代码。
int func(int x) {
int z = 1337;
return z;
}
[使用GCC -O0,每个C变量都有一个未优化的内存地址,因此gcc溢出了其寄存器arg:(Godbolt, gcc5.5 -O0 -fverbose-asm)
func:
pushq %rbp #
movq %rsp, %rbp #,
movl %edi, -20(%rbp) # x, x
movl $1337, -4(%rbp) #, z
movl -4(%rbp), %eax # z, D.2332
popq %rbp #
ret
函数参数x放置在局部变量下方的堆栈上的原因是什么?为什么不将其放置在-4(%rbp)
及其附近的地方?
并且将其放置在局部变量下方时,为什么不将其放置在-8(%rbp)
?
[为什么要留出空白,使用比必要多的red-zone?此操作是否不能触及本叶子函数无法触及的新缓存行?
(首先,不要指望在-O0
处做出有效的决策。事实证明,如果我们使用-O0
或其他方式来强制编译器执行以下操作,则您在-O3
处注意到的事情仍然会在volatile
处发生。分配堆栈空间,否则这个问题就不会那么有趣了。)
函数参数x放置在局部变量下方的堆栈上的原因是什么?
选择是100%任意的,取决于编译器内部。 GCC和clang都碰巧做出了这样的选择,但这基本上无关紧要。 args到达寄存器,基本上are只是局部变量,因此完全由编译器来决定将它们溢出的位置(或者,如果启用优化则根本不溢出)。
但是为什么要在真正必要的时间之后将其保存在堆栈中呢?
由于已知的GCC遗失优化错误导致浪费堆栈空间。(待办事项:找到gcc bugzilla的链接;我知道我以前见过有关此错误的报告。)
注意,x86-64 System V ABI在call
之前要求16字节堆栈对齐。在push %rbp
并将RBP设置为帧指针之后,RBP和RSP对齐16字节。 -20(%rbp)
与-8(%rbp)
位于同一对齐的16字节堆栈空间块中,因此该间隙不会冒接触我们尚未接触过的新缓存行或页面的风险。 (自然对齐的内存块不能跨越比其自身更宽的任何边界,x86-64高速缓存行始终至少为32个字节;如今,始终为64个字节。)
但是,如果我们添加第二个参数int y
,此does会成为错过的优化方法:gcc5.5(和当前的gcc9.2 -O0)会将其溢出到-24(%rbp)
,而这可能在新的缓存中行。
事实证明,错过的优化是not,只是因为您使用了-O0
(快速编译,跳过大多数优化过程,make bad asm)。除非它们仍然以任何人关心的优化级别出现,尤其是-O0
,-Os
或-O2
,否则在-O3
输出中查找错过的优化是没有意义的。
[[我们可以用使用volatile
的代码仍然证明gcc在-O3
处为args / locals分配堆栈空间的代码来证明这一点>另一个选择是将其地址传递给另一个函数,但是GCC会保留空间,而不仅仅是使用RSP下方的红色区域。
int *volatile sink;
int func(int x, int y) {
sink = &x;
sink = &y;
int z = 1337;
sink = &z;
return z;
}
(Godbolt, gcc9.2)
gcc9.2 -O3 (hand-edited comments) func(int, int): leaq -20(%rsp), %rax # &x movq %rax, sink(%rip) # tmp84, sink leaq -24(%rsp), %rax # &y movq %rax, sink(%rip) # tmp86, sink leaq -4(%rsp), %rax # &z movq %rax, sink(%rip) # tmp88, sink movl $1337, %eax #, ret sink: .zero 8
有趣的事实:clang -O3在将其地址存储到sink
之前溢出了堆栈args,就像它是该地址的std::atomic
释放存储区一样,另一个线程可能在从sink
获取指针后可能会加载它们的值。但是对于z
,它不这样做。实际溢出x
和y
只是一个错过的优化,我只能推测应该归咎于clang内部机械的哪一部分。无论如何,clang确实将
z
分配给-4(%rsp)
,将-8分配给x
,将y
分配给-12
。因此,无论出于何种原因,clang都选择将args的溢出位置放置在本地变量下方。