我尝试查看一个非常简单的程序的汇编代码。
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
或其他东西来强制编译器分配堆栈空间,否则这个问题就会变得不那么有趣。)
What is the reason that the function parameter x gets placed on the stack below the local variables?
该选择是 100% 任意的,并且取决于编译器内部结构。 GCC 和 clang 都恰好做出了这样的选择,但这基本上是无关紧要的。 args 到达寄存器,基本上只是局部变量,因此完全由编译器决定将它们溢出到哪里(或者根本不溢出,如果启用优化)。
- Order of local variable allocation on the stack链接 ftp://gcc.gnu.org/pub/gcc/summit/2003/Optimal%20Stack%20Slot%20Assignment.pdf
But why save it further down the stack later than really necessary?
由于已知的(?)GCC 错过优化错误导致堆栈空间浪费。 例如,Why does GCC allocate more space than necessary on the stack?演示了 x86-64 GCC -O3
分配 24 个字节而不是 8 个字节的堆栈空间,其中 clang 分配 8 个。(我想我已经看到了一个关于有时在 GCC 时使用额外 16 个字节的空间的错误报告需要移动 RSP(与这里不同,它只是使用红色区域),但在 GCC bugzilla 上找不到它。)
请注意,x86-64 System V ABI 要求在调用
之前进行 16 字节堆栈对齐。 push %rbp
并将 RBP 设置为帧指针后,RBP 和 RSP 是 16 字节对齐的。 -20(%rbp)
与 -8(%rbp)
位于同一对齐的 16 字节堆栈空间 block 中,因此此间隙不会有触及新缓存的风险我们还没有接触过的行或页。 (自然对齐的内存块不能跨越任何比自身更宽的边界,x86-64 缓存行始终至少为 32 字节;现在始终为 64 字节。)
但是,如果我们添加第二个参数 int y
,这确实会成为一个错过的优化:gcc5.5(和当前的 gcc9.2 -O0)将其溢出到-24(%rbp)
可能位于新的缓存行中。
事实证明,这种错过的优化不仅仅是只是因为您使用了-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
之前会溢出堆栈参数,就像它是地址和另一个的 std::atomic
释放存储一样线程可以在从接收器获取指针后加载它们的值。但它不会对 z
执行此操作。这只是一个错过的优化,实际上溢出了 x
和 y
,我只能推测 clang 内部机制的哪一部分可能是罪魁祸首。
无论如何,clang确实在-4(%rsp)
分配了z
,在-8,y
分配了x
> 在-12
。因此,无论出于何种原因,clang 还选择将参数的溢出槽放在局部变量下方。
相关:
Waste in memory allocation for local variables讨论 GCC 的
main
在进入main
时不假设 16 字节对齐。关于 GCC 为变量分配额外堆栈空间的几个可能的重复,但大多只是按照对齐的要求,而不是额外的。
关于c - GCC 将寄存器 args 放置在堆栈上,并在局部变量下方留有间隙?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58631698/