函数所需的堆栈 space 会影响 C/C++ 中的内联决策吗?
Does stack space required by a function affect inlining decisions in C/C++?
函数所需的大量堆栈 space 会阻止它被内联吗?比如如果我在堆栈上有一个 10k 的自动缓冲区,那会不会使函数不太可能被内联?
int inlineme(int args) {
char svar[10000];
return stringyfunc(args, svar);
}
我更关心 gcc,但 icc 和 llvm 也很高兴知道。
我知道这不太理想,但我很好奇。该代码也可能在缓存上也很糟糕。
是的,内联与否取决于函数的复杂性、堆栈和寄存器的使用情况以及进行调用的上下文。这些规则依赖于编译器和目标平台。当性能很重要时,请始终检查生成的程序集。
比较 this version 与 10000 个字符的数组 not 被内联(GCC 8.2、x64、-O2):
inline int inlineme(int args) {
char svar[10000];
return stringyfunc(args, svar);
}
int test(int x) {
return inlineme(x);
}
生成的程序集:
inlineme(int):
sub rsp, 10008
mov rsi, rsp
call stringyfunc(int, char*)
add rsp, 10008
ret
test(int):
jmp inlineme(int)
with this one 有一个小得多的 10 字符数组,其中 是 内联的:
inline int inlineme(int args) {
char svar[10];
return stringyfunc(args, svar);
}
int test(int x) {
return inlineme(x);
}
生成的程序集:
test(int):
sub rsp, 24
lea rsi, [rsp+6]
call stringyfunc(int, char*)
add rsp, 24
ret
Such as if I had a 10k automatic buffer on the stack, would that make the function less likely to be inlined?
一般情况下不一定。事实上,内联扩展有时可以减少堆栈 space 的使用,因为不必为函数参数设置 space。
将 "wide" 调用扩展到调用其他 "wide" 函数的单个框架中可能是个问题,除非优化器单独防止这种情况,否则可能必须避免扩展 "wide" 功能一般。
在递归的情况下:很可能是。
LLVM source的例子:
if (IsCallerRecursive &&
AllocatedSize > InlineConstants::TotalAllocaSizeRecursiveCaller) {
InlineResult IR = "recursive and allocates too much stack space";
来自GCC source:
For stack growth limits we always base the growth in stack usage
of the callers. We want to prevent applications from segfaulting
on stack overflow when functions with huge stack frames gets
inlined.
控制极限,来自GCC manual:
--param name=value
large-function-growth
- Specifies maximal growth of large function caused by inlining in percents. For example, parameter value 100 limits large function growth to 2.0 times the original size.
large-stack-frame
- The limit specifying large stack frames. While inlining the algorithm is trying to not grow past this limit too much.
large-stack-frame-growth
- Specifies maximal growth of large stack frames caused by inlining in percents. For example, parameter value 1000 limits large stack frame growth to 11 times the original size.
是的,部分原因是编译器在 prologue/epilogue 中为整个函数进行一次堆栈分配,而不是在 enter/leave 块范围时移动堆栈指针。
and each inlined call to inlineme() would need its own buffer.
不,我很确定编译器足够聪明,可以为同一函数的不同实例重用相同的堆栈space,因为该 C 变量的实例只能是立即在范围内。
内联后的优化可以将内联函数的一些操作合并到调用代码中,但我认为编译器很少会以它想要同时保留的数组的 2 个版本结束。
I don't see why that would be a concern for inlineing. Can you give an example of how functions that require a lot of stack would be problematic to inline?
它可能产生的问题的真实示例(编译器启发式方法通常会避免):
将if (rare_special_case) use_much_stack()
内联到递归函数中,否则不会使用太多堆栈将是一个明显的性能问题(更多缓存和 TLB 未命中),甚至如果递归深度足以实际溢出堆栈,则正确性。
(特别是在像 Linux 内核堆栈这样的受限环境中,每个线程通常为 8kiB 或 16kiB,高于旧 Linux 版本的 32 位平台上的 4k。https://elinux.org/Kernel_Small_Stacks 有关于尝试摆脱 4k 堆栈的一些信息和历史引述,因此内核不必为每个任务找到 2 个连续的物理页面。
编译器通常让函数分配所有堆栈 space 他们将永远需要预先(除了 VLA 和 alloca
)。内联错误处理或特殊情况处理函数,而不是在极少数需要的情况下调用它 将分配大量堆栈 (通常 save/restore 更多调用-保留的寄存器)在主要 prologue/epilogue、 中它也影响快速路径 。特别是如果快速路径没有进行任何其他函数调用。
如果您不内联处理程序,那么如果没有错误(或没有发生特殊情况),将永远不会使用该堆栈 space。所以快速路径可以更快,使用更少的 push/pop 指令并且在继续调用另一个函数之前不分配任何大缓冲区。 (即使函数本身实际上不是递归的,在深度调用树中的多个函数中发生这种情况也会浪费大量堆栈。)
我读到 Linux 内核确实在 gcc 的内联试探法做出不希望的内联决定的几个关键位置手动执行此优化:通过调用慢速路径将函数分解为快速路径,并在较大的慢速路径函数上使用 __attribute__((noinline))
以确保它不会内联。
在某些情况下,不在条件块内进行单独分配是错过了优化,但是更多的堆栈指针操作会使堆栈展开元数据以支持异常(和回溯)更加臃肿(尤其是 saving/restoring 的调用保留寄存器,必须恢复异常堆栈展开)。
如果您在 运行 之前在条件块内进行保存 and/or 分配,一些公共代码可以通过任何一种方式到达(使用另一个分支来决定在结语中恢复哪些寄存器),那么如果没有某种可以指示寄存器或内存位置的极其复杂的元数据格式,异常处理程序机制将无法知道是否仅从该函数保存它们的位置加载 R12 或 R13(例如)对某些条件进行测试。 ELF 可执行文件/库中的 .eh_frame
部分已经足够臃肿了! (它是非可选的,顺便说一句。x86-64 System V ABI(例如)甚至在不支持异常的代码中或在 C 中也需要它。在某些方面这很好,因为这意味着回溯通常有效,甚至通过通过函数备份的异常会导致损坏。)
不过,您绝对可以在条件块内调整堆栈指针。为 32 位 x86 编译的代码(带有蹩脚的堆栈参数调用约定)甚至可以并且确实在条件分支内使用 push
。所以只要你在离开分配space的块之前清理堆栈,这是可行的。那不是 saving/restoring 寄存器,只是移动堆栈指针。 (在没有帧指针构建的函数中,展开元数据必须记录所有此类更改,因为堆栈指针是查找已保存寄存器和 return 地址的唯一参考。)
我不确定关于为什么编译器不能/不想更聪明地只在使用它的块内分配大的额外堆栈 space 的详细信息。问题的很大一部分可能是他们的内部结构甚至没有设置为能够寻找这种优化。
相关:Raymond Chen posted a blog about the PowerPC calling convention, and how there are specific requirements on function prologues / epilogues that make stack unwinding work. (And the rules imply / require the existence of a red zone below the stack pointer that's safe from async clobber. A few other calling conventions use red zones, like x86-64 System V, but Windows x64 doesn't. Raymond posted another blog about red zones)
函数所需的大量堆栈 space 会阻止它被内联吗?比如如果我在堆栈上有一个 10k 的自动缓冲区,那会不会使函数不太可能被内联?
int inlineme(int args) {
char svar[10000];
return stringyfunc(args, svar);
}
我更关心 gcc,但 icc 和 llvm 也很高兴知道。
我知道这不太理想,但我很好奇。该代码也可能在缓存上也很糟糕。
是的,内联与否取决于函数的复杂性、堆栈和寄存器的使用情况以及进行调用的上下文。这些规则依赖于编译器和目标平台。当性能很重要时,请始终检查生成的程序集。
比较 this version 与 10000 个字符的数组 not 被内联(GCC 8.2、x64、-O2):
inline int inlineme(int args) {
char svar[10000];
return stringyfunc(args, svar);
}
int test(int x) {
return inlineme(x);
}
生成的程序集:
inlineme(int):
sub rsp, 10008
mov rsi, rsp
call stringyfunc(int, char*)
add rsp, 10008
ret
test(int):
jmp inlineme(int)
with this one 有一个小得多的 10 字符数组,其中 是 内联的:
inline int inlineme(int args) {
char svar[10];
return stringyfunc(args, svar);
}
int test(int x) {
return inlineme(x);
}
生成的程序集:
test(int):
sub rsp, 24
lea rsi, [rsp+6]
call stringyfunc(int, char*)
add rsp, 24
ret
Such as if I had a 10k automatic buffer on the stack, would that make the function less likely to be inlined?
一般情况下不一定。事实上,内联扩展有时可以减少堆栈 space 的使用,因为不必为函数参数设置 space。
将 "wide" 调用扩展到调用其他 "wide" 函数的单个框架中可能是个问题,除非优化器单独防止这种情况,否则可能必须避免扩展 "wide" 功能一般。
在递归的情况下:很可能是。
LLVM source的例子:
if (IsCallerRecursive && AllocatedSize > InlineConstants::TotalAllocaSizeRecursiveCaller) { InlineResult IR = "recursive and allocates too much stack space";
来自GCC source:
For stack growth limits we always base the growth in stack usage of the callers. We want to prevent applications from segfaulting on stack overflow when functions with huge stack frames gets inlined.
控制极限,来自GCC manual:
--param name=value
large-function-growth
- Specifies maximal growth of large function caused by inlining in percents. For example, parameter value 100 limits large function growth to 2.0 times the original size.
large-stack-frame
- The limit specifying large stack frames. While inlining the algorithm is trying to not grow past this limit too much.
large-stack-frame-growth
- Specifies maximal growth of large stack frames caused by inlining in percents. For example, parameter value 1000 limits large stack frame growth to 11 times the original size.
是的,部分原因是编译器在 prologue/epilogue 中为整个函数进行一次堆栈分配,而不是在 enter/leave 块范围时移动堆栈指针。
and each inlined call to inlineme() would need its own buffer.
不,我很确定编译器足够聪明,可以为同一函数的不同实例重用相同的堆栈space,因为该 C 变量的实例只能是立即在范围内。
内联后的优化可以将内联函数的一些操作合并到调用代码中,但我认为编译器很少会以它想要同时保留的数组的 2 个版本结束。
I don't see why that would be a concern for inlineing. Can you give an example of how functions that require a lot of stack would be problematic to inline?
它可能产生的问题的真实示例(编译器启发式方法通常会避免):
将if (rare_special_case) use_much_stack()
内联到递归函数中,否则不会使用太多堆栈将是一个明显的性能问题(更多缓存和 TLB 未命中),甚至如果递归深度足以实际溢出堆栈,则正确性。
(特别是在像 Linux 内核堆栈这样的受限环境中,每个线程通常为 8kiB 或 16kiB,高于旧 Linux 版本的 32 位平台上的 4k。https://elinux.org/Kernel_Small_Stacks 有关于尝试摆脱 4k 堆栈的一些信息和历史引述,因此内核不必为每个任务找到 2 个连续的物理页面。
编译器通常让函数分配所有堆栈 space 他们将永远需要预先(除了 VLA 和 alloca
)。内联错误处理或特殊情况处理函数,而不是在极少数需要的情况下调用它 将分配大量堆栈 (通常 save/restore 更多调用-保留的寄存器)在主要 prologue/epilogue、 中它也影响快速路径 。特别是如果快速路径没有进行任何其他函数调用。
如果您不内联处理程序,那么如果没有错误(或没有发生特殊情况),将永远不会使用该堆栈 space。所以快速路径可以更快,使用更少的 push/pop 指令并且在继续调用另一个函数之前不分配任何大缓冲区。 (即使函数本身实际上不是递归的,在深度调用树中的多个函数中发生这种情况也会浪费大量堆栈。)
我读到 Linux 内核确实在 gcc 的内联试探法做出不希望的内联决定的几个关键位置手动执行此优化:通过调用慢速路径将函数分解为快速路径,并在较大的慢速路径函数上使用 __attribute__((noinline))
以确保它不会内联。
在某些情况下,不在条件块内进行单独分配是错过了优化,但是更多的堆栈指针操作会使堆栈展开元数据以支持异常(和回溯)更加臃肿(尤其是 saving/restoring 的调用保留寄存器,必须恢复异常堆栈展开)。
如果您在 运行 之前在条件块内进行保存 and/or 分配,一些公共代码可以通过任何一种方式到达(使用另一个分支来决定在结语中恢复哪些寄存器),那么如果没有某种可以指示寄存器或内存位置的极其复杂的元数据格式,异常处理程序机制将无法知道是否仅从该函数保存它们的位置加载 R12 或 R13(例如)对某些条件进行测试。 ELF 可执行文件/库中的 .eh_frame
部分已经足够臃肿了! (它是非可选的,顺便说一句。x86-64 System V ABI(例如)甚至在不支持异常的代码中或在 C 中也需要它。在某些方面这很好,因为这意味着回溯通常有效,甚至通过通过函数备份的异常会导致损坏。)
不过,您绝对可以在条件块内调整堆栈指针。为 32 位 x86 编译的代码(带有蹩脚的堆栈参数调用约定)甚至可以并且确实在条件分支内使用 push
。所以只要你在离开分配space的块之前清理堆栈,这是可行的。那不是 saving/restoring 寄存器,只是移动堆栈指针。 (在没有帧指针构建的函数中,展开元数据必须记录所有此类更改,因为堆栈指针是查找已保存寄存器和 return 地址的唯一参考。)
我不确定关于为什么编译器不能/不想更聪明地只在使用它的块内分配大的额外堆栈 space 的详细信息。问题的很大一部分可能是他们的内部结构甚至没有设置为能够寻找这种优化。
相关:Raymond Chen posted a blog about the PowerPC calling convention, and how there are specific requirements on function prologues / epilogues that make stack unwinding work. (And the rules imply / require the existence of a red zone below the stack pointer that's safe from async clobber. A few other calling conventions use red zones, like x86-64 System V, but Windows x64 doesn't. Raymond posted another blog about red zones)