以下是人为的例子。显然,编译器优化将极大地改变最终结果。但是,我不能强调这一点:通过暂时禁用优化,我打算在堆栈使用上有一个上限,可能,我希望进一步的编译器优化可以改善这种情况。
仅以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
已被释放?
除了函数调用(导致额外的堆栈使用)之外,还有什么办法可以很好地控制堆栈释放吗?
答案 0 :(得分:5)
我希望能够很好地控制自动变量从堆栈中释放的方式。
这里有很多混乱。 optimizing compiler只能在automatic variables中存储一些registers,而不使用调用框架中的任何插槽。 C语言规范(n1570)不需要任何调用堆栈。
并且调用帧中的给定寄存器或槽可以用于不同目的(例如,函数的不同部分中的不同自动变量)。 Register allocation是编译器的重要角色。
我是否总能指望在测量点B,堆栈只包含缓冲区2并且缓冲区1已被释放?
当然不是。编译器可以证明,在代码的某个稍后阶段,buffer1
的空间不再有用,因此将该空间重用于其他目的。
有什么方法可以很好地控制堆栈解除分配吗?
否,没有。 call stack是一个实现细节,可能不会被编译器和生成的代码使用(或者在您的观点中被滥用")。
对于一些愚蠢的例子,如果buffer1
中没有使用foo
,编译器可能不会为它分配空间。一些聪明的编译器可能只在其中分配8个字节,如果它们可以证明只有buffer1
的8个第一个字节是有用的。
更严重的是,在某些案例中,GCC可以进行tail-call优化。
您应该对-fstack-reuse=all
,-Os
感兴趣invoking GCC,
-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上使用gcc -S -fverbose-asm -O2 e.c
使用GCC8.1编译时在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)。
答案 1 :(得分:4)
我是否总能指望在测量B时,堆栈只包含缓冲区2并且缓冲区1已被释放?
没有。它将取决于GCC版本,目标,优化级别,选项。
除了函数调用(导致额外的堆栈使用)之外,还有什么办法可以很好地控制堆栈释放吗?
您的要求是如此具体,我猜您可能必须自己编写汇编程序中的代码。
答案 2 :(得分:1)
mov BYTE PTR [rbp-20], 1
和mov BYTE PTR [rbp-10], 2
仅显示堆栈帧中堆栈指针的相对偏移量。在考虑运行时情况时,它们具有相同的峰值堆栈使用率。
使用inline
有两个不同之处:
1)在函数调用模式下,从foo()退出时将释放buffer1。但是在内联方法中,缓冲区1在从bar()退出之前不会被保留,这意味着峰值堆栈的使用将持续更长的时间。 2)函数调用会增加一些开销,例如保存堆栈帧信息,与内联模式进行比较