代码之家  ›  专栏  ›  技术社区  ›  Juan Leni

内联和堆栈帧控件

  •  5
  • Juan Leni  · 技术社区  · 6 年前

    以下是人为的例子。显然,编译器优化将显著改变最终结果。但是,我不能再强调这一点: 通过暂时禁用优化,我打算在堆栈使用上有一个上限 很可能,我希望进一步的编译器优化可以改善这种情况。

    讨论仅围绕GCC展开。我希望能够很好地控制自动变量如何从堆栈中释放。使用块的作用域不能确保在自动变量超出作用域时释放内存。据我所知,功能确实确保了这一点。

    但是,当内联时,情况如何?例如:

    inline __attribute__((always_inline)) void foo()
    {
        uint8_t buffer1[100];
        // Stack Size Measurement A
        // Do something 
    }
    
    void bar()
    {
        foo();
        uint8_t buffer2[100];
        // Stack Size Measurement B
        // Do something else
    }
    

    我可以吗 总是 预计在测量点B,堆栈将只包含 buffer2 buffer1 已经发布?

    除了函数调用(这会导致额外的堆栈使用)之外,还有什么方法可以对堆栈释放进行精细控制?

    3 回复  |  直到 6 年前
        1
  •  5
  •   Basile Starynkevitch    6 年前

    我希望能够很好地控制自动变量如何从堆栈中释放。

    这里有很多混乱。这个 optimizing compiler 可以储存一些 automatic variables 只有在 registers ,而不使用呼叫帧中的任何插槽。C语言规范( n1570 )不需要任何调用堆栈。

    并且给定的寄存器或调用帧中的槽可以被重用用于不同的目的(例如函数的不同部分中的不同自动变量)。 Register allocation 是编译器的重要角色。

    我能一直期望在测量点B,堆栈将只包含缓冲区2和缓冲区1已被释放吗?

    当然不是。编译器可以证明,在您的代码中,稍后的某个时间点 buffer1 不再有用,所以可以将该空间用于其他用途。

    我有没有办法对堆栈释放进行精细控制?

    没有。这个 call stack 是一个实现细节,可能不会被编译器和生成的代码使用(或者在您的观点中被“滥用”)。

    例如,如果 缓冲区1 不用于 foo 编译器可能没有为其分配空间。一些聪明的编译器可能只在其中分配8个字节,如果他们能够证明 缓冲区1 是有用的。

    更严重的是, 一些 案例,GCC能够做到 tail-call 优化。

    你应该对 invoking GCC 具有 -fstack-reuse=all , -Os , -Wstack-usage=256 , -fstack-usage 和其他选项。

    当然,具体的堆栈使用取决于优化级别。您还可以检查生成的汇编程序代码,例如 -S -O2 -fverbose-asm

    例如,以下代码 e.c :

    int f(int x, int y) {
        int t[100];
        t[0] = x;
        t[1] = y;
        return t[0]+t[1];
    }
    

    在Linux/Debian/x86-64上使用GCC8.1编译时,使用 gcc -S -fverbose-asm -O2 e.c 屈服 e.s

            .text
            .p2align 4,,15
            .globl  f
            .type   f, @function
    f:
    .LFB0:
            .cfi_startproc
    # e.c:5:      return t[0]+t[1];
            leal    (%rdi,%rsi), %eax       #, tmp90
    # e.c:6: }
            ret     
            .cfi_endproc
    .LFE0:
            .size   f, .-f
    

    你看到堆栈框架是 增加100*4字节。以下情况仍然如此:

    int f(int x, int y, int n) {
        int t[n];
        t[0] = x;
        t[1] = y;
        return t[0]+t[1];
    }
    

    它实际上生成与上面相同的机器代码。如果不是 + 上面我打电话给 inline int add(int u, int v) { return u+v; } 生成的代码没有更改。

    注意 as-if rule 以及 undefined behavior (如果 n 是上面的1,它是ub)。

        2
  •  4
  •   Yann Droneaud    6 年前

    我能一直期望在测量B时,堆栈将只包含缓冲区2和缓冲区1已释放吗?

    不,这取决于GCC版本、目标、优化级别和选项。

    除了函数调用(这会导致额外的堆栈使用)之外,还有什么方法可以对堆栈释放进行精细控制?

    您的要求非常具体,我想您可能需要用汇编程序编写代码。

        3
  •  1
  •   gzh    6 年前

    mov BYTE PTR [rbp-20], 1 mov BYTE PTR [rbp-10], 2 仅在堆栈帧中显示堆栈指针的相对偏移量。在考虑运行时情况时,它们具有相同的峰值堆栈使用率。

    关于是否使用 inline : 1)在函数调用模式下,当退出foo()时,buffer1将被释放。但在inline方法中,buffer1在从bar()退出之前不会保留,这意味着峰值堆栈使用将持续更长的时间。2)函数调用会增加一些开销,例如保存堆栈帧信息,与内联模式相比