是的,部分原因是编译器在序言/尾声中为整个函数分配了一次堆栈,而不是在堆栈指针进入/离开块范围时移动堆栈指针。
每个对 inlineme() 的内联调用都需要自己的缓冲区。
不,我很确定编译器足够聪明,可以为同一函数的不同实例重用相同的堆栈空间,因为该 C 变量的一个实例一次只能在作用域内。强>
内联后的优化可以将内联函数的一些操作合并到调用代码中,但我认为编译器很少会最终得到它想要同时保留的数组的两个版本。
我不明白为什么这会成为内联的问题。您能否举例说明需要大量堆栈的函数如何内联?
它可能产生的问题的真实示例(编译器启发式通常会避免):
将 if (rare_special_case) use_much_stack() 内联到递归函数中,否则不会使用太多堆栈,这将是一个明显的性能问题(更多缓存和 TLB 未命中),如果递归足够深,甚至正确性实际上溢出了堆栈。
(特别是在 Linux 内核堆栈等受限环境中,通常每个线程 8kiB 或 16kiB,而旧版 Linux 的 32 位平台上为 4k。https://elinux.org/Kernel_Small_Stacks 有一些关于试图摆脱 4k 的信息和历史引述堆栈,因此内核不必为每个任务查找 2 个连续的物理页)。
编译器通常会让函数预先分配它们需要的所有堆栈空间(VLA 和alloca 除外)。内联错误处理或特殊情况处理函数,而不是在极少数情况下调用它会在主要的序幕/尾声,它也会影响快速路径。特别是如果快速路径没有进行任何其他函数调用。
如果您不内联处理程序,那么如果没有错误(或特殊情况没有发生),则永远不会使用该堆栈空间。因此,快速路径可以更快,使用更少的推送/弹出指令,并且在继续调用另一个函数之前不分配任何大缓冲区。 (即使函数本身实际上不是递归的,在深度调用树中的多个函数中发生这种情况也可能会浪费大量堆栈。)
我读到 Linux 内核确实在几个关键位置手动执行此优化,在这些地方 gcc 的内联启发式会做出不想要的内联决定:将函数分解为快速路径调用慢速路径,并在较大的慢速路径函数上使用__attribute__((noinline)) 以确保它不会内联。
在某些情况下,不在条件块内进行单独分配会错过优化,但更多的堆栈指针操作会使堆栈展开元数据以支持异常(和回溯)更加臃肿(尤其是保存/恢复堆栈展开异常的调用保留寄存器必须恢复)。
如果您在运行某些通过任一方式到达的通用代码之前在条件块内进行保存和/或分配(使用另一个分支来决定在结语中恢复哪些寄存器),那么将无法异常处理程序机器知道是仅加载 R12 还是 R13 以及(例如)从该函数保存它们的位置,而不需要某种异常复杂的元数据格式,可以指示寄存器或内存位置以针对某些条件进行测试。 ELF 可执行文件/库中的.eh_frame 部分已经足够臃肿了! (顺便说一句,它是非可选的。x86-64 System V ABI(例如)即使在不支持异常的代码或 C 中也需要它。在某些方面这很好,因为这意味着回溯通常可以工作,甚至通过通过函数备份的异常会导致损坏。)
不过,您绝对可以在条件块内调整堆栈指针。为 32 位 x86 编译的代码(带有糟糕的堆栈参数调用约定)可以并且确实使用 push 甚至在条件分支中。所以只要你在离开分配空间的块之前清理堆栈,它是可行的。那不是保存/恢复寄存器,只是移动堆栈指针。 (在没有帧指针构建的函数中,展开元数据必须记录所有这些变化,因为堆栈指针是查找保存的寄存器和返回地址的唯一参考。)
我不确定为什么编译器不能/不想更智能地仅在使用它的块内分配大量额外堆栈空间的详细信息。问题的一个重要部分可能是它们的内部结构没有设置为甚至能够寻找这种优化。
相关:Raymond Chen posted a blog 关于 PowerPC 调用约定,以及如何对使堆栈展开工作的函数序言/尾声有特定要求。 (并且规则暗示/要求在堆栈指针下方存在一个红色区域,该区域对异步破坏是安全的。其他一些调用约定使用红色区域,例如 x86-64 System V,但 Windows x64 没有。Raymond 发布 @987654323 @)