函数所需的堆栈空间是否会影响C / C ++中的内联决策?

问题描述 投票:6回答:3

函数需要大量的堆栈空间是否会阻止它内联?例如,如果我在堆栈上有一个10k自动缓冲区,是否会使该函数不太可能被内联?

int inlineme(int args) {
  char svar[10000];

  return stringyfunc(args, svar);
}

我更关心gcc,但icc和llvm也很高兴知道。

我知道这不太理想,但我很好奇。缓存上的代码很可能也很糟糕。

c++ c gcc inline micro-optimization
3个回答
6
投票

是的,内联与否的决定取决于函数的复杂性,其堆栈和寄存器的使用以及调用的上下文。规则依赖于编译器和目标平台。当性能很重要时,请务必检查生成的组件

this version与未内联的10000字符数组进行比较(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)

this one一起使用更小的10-char数组,内联:

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

4
投票

例如,如果我在堆栈上有一个10k自动缓冲区,是否会使该函数不太可能被内联?

不一定一般。事实上,内联扩展有时可以减少堆栈空间的使用,因为不必为函数参数设置空间。

将“宽”调用扩展为调用其他“宽”函数的单个帧可能是一个问题,除非优化器单独防范,否则它可能必须避免扩展“宽”函数。

在递归的情况下:很可能是的。

LLVM source的一个例子:

if (IsCallerRecursive &&
         AllocatedSize > InlineConstants::TotalAllocaSizeRecursiveCaller) {
       InlineResult IR = "recursive and allocates too much stack space";

来自GCC source

对于堆栈增长限制,我们总是以调用者的堆栈使用量为基础。当具有大堆栈帧的函数被内联时,我们希望阻止应用程序对堆栈溢出进行分段。

控制限制,来自GCC manual

--param name = value

大函数增长

  • 指定由百分比内联引起的大函数的最大增长。例如,参数值100将大功能增长限制为原始大小的2.0倍。

大堆栈帧

  • 指定大堆栈帧的限制。虽然内联算法试图不超过这个限制。

大堆栈帧生长

  • 指定由百分比内联引起的大堆栈帧的最大增长。例如,参数值1000将大堆栈帧增长限制为原始大小的11倍。

2
投票

是的,部分原因是编译器在序言/尾声中对整个函数进行堆栈分配,而不是在进入/离开块作用域时移动堆栈指针。


每个内联调用inlineme()都需要自己的缓冲区。

不,我非常确定编译器足够聪明,可以为同一个函数的不同实例重用相同的堆栈空间,因为只有该C变量的一个实例可以同时在范围内。

内联后的优化可以将内联函数的一些操作合并到调用代码中,但我认为编译器很少会同时拥有它想要保留的2个版本的数组。

我不明白为什么这会引起内联的关注。您能举例说明需要大量堆栈的函数如何成为内联问题?

它可能创建的问题的一个真实示例(编译器启发式主要避免):

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编译的代码(使用糟糕的stack-args调用约定)甚至可以在条件分支中使用push。因此,只要在离开分配空间的块之前清理堆栈,就可以了。这不是保存/恢复寄存器,只是移动堆栈指针。 (在没有帧指针的情况下构建的函数中,展开元数据必须记录所有这些更改,因为堆栈指针是查找已保存寄存器和返回地址的唯一参考。)

我不确定为什么编译器不能/不希望更聪明地在使用它的块内部分配大量额外堆栈空间的详细信息。可能问题的一个很大一部分就是他们的内部结构不是为了能够甚至找不到这种优化。


相关:Raymond Chen posted a blog关于PowerPC调用约定,以及如何对函数序言/结尾进行特定要求,使堆栈展开工作。 (规则暗示/要求在堆栈指针下面存在一个红色区域,这个区域可以安全地避免异步删除。其他一些调用约定使用红色区域,例如x86-64 System V,但Windows x64没有.Raymond发布了another blog about red zones

© www.soinside.com 2019 - 2024. All rights reserved.