c - GCC 将寄存器 args 放置在堆栈上,并在局部变量下方留有间隙?

标签 c gcc assembly x86 compiler-optimization

我尝试查看一个非常简单的程序的汇编代码。

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) 处?

为什么要留一个间隙,使用更多的比必要的?难道这不能触及一个新的缓存行,否则该叶函数中不会触及该行吗?

最佳答案

(首先,不要指望在 -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 到达寄存器,基本上只是局部变量,因此完全由编译器决定将它们溢出到哪里(或者根本不溢出,如果启用优化)。

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 执行此操作。这只是一个错过的优化,实际上溢出了 xy,我只能推测 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/

相关文章:

c - 为什么 C 程序返回一个 int?

c - 何时使用 QueueUserAPC()?

c++ - 为什么 gcc 和 clang 会为成员函数模板参数生成非常不同的代码?

编码标准 : variable initialization

c - 在 64 位 arch 中使用寄存器访问参数

c - 打印动态数组时得到错误的输出

ios - 在 iOS 应用程序中的 fgets 上获取 EXC_BAD_ACCESS

c++ - GCC 优化返回值

linux - 执行参数错误

c - 程序集 cmp 如何检查它比较的值?