【问题标题】:What are guiding principles of expansion of callee inside the caller (Inlining - Compiler Optimization) [duplicate]在调用者内部扩展被调用者的指导原则是什么(内联 - 编译器优化)[重复]
【发布时间】:2015-07-25 06:11:15
【问题描述】:

我的理解是编译器遵循某些语义来决定函数是否应该内联扩展。例如,如果被调用者无条件(不返回 if/élse-if)返回一个值,则它可能会在调用者本身中扩展。同样,函数调用开销也可以引导这种扩展。(我可能完全错了)

同样,缓存使用等硬件参数也可能在扩展中发挥作用。

作为一名程序员,我想了解这些语义和指导内联扩展的算法。最终,我应该能够编写(或识别)肯定会被内联(非内联)的代码。我并不是要覆盖编译器,或者我认为我可以编写比编译器本身更好的代码。问题是要了解编译器的内部结构。

编辑:由于我在工作中使用 gcc/g++,我们可以将范围限制在这两个范围内。不过,我认为在这种情况下,编译器之间会有一些共同点。

【问题讨论】:

  • 你明白不同的编译器处理它的方式不同吧?如果它是虚拟的,有些可能会跳过一个函数,有些可能不会。大多数做递归内联,有些不做。问题需要缩小到“特定编译器(例如 gcc)如何内联函数”或其他无法回答的问题。看看:gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Optimize-Options.html
  • 你正在走微优化的道路,这不是一个好的起点,因为人们忽视了大局
  • 其实你完全错了!
  • @CamelToe:GCC 4.1 已经过时了,你最好引用最新的 GCC 文档...
  • “最终,我应该能够编写肯定会被内联的代码”——你不应该。如果编译器没有内联函数,它有一个很好的理由(例如保存缓存)。编写始终内联的代码并不能实现最终的性能提升;内联并不是一种神奇的通用性能提升技术。如果你对编译器诚实,你会获得最好的结果,不要对它撒谎,让它根据你最初打算编写的代码做出决定。

标签: c++ c caching inline compiler-optimization


【解决方案1】:

不需要了解内联(或其他优化)标准,因为根据定义(假设 optimizing compiler 在这方面没有问题),内联代码应该与非内联代码的行为相同

你的第一个例子(被调用者无条件返回一个值)在实践中肯定是错误的,因为几个编译器能够内联条件返回。

例如,考虑这个f.c 文件:

static int fact (int n) {
  if (n <= 0) return 1;
  else
    return n * fact (n - 1);
}

int foo () {
  return fact (10);
}

gcc -O3 -fverbose-asm -S f.c编译它;生成的 f.s 程序集文件仅包含 一个 函数 (foo),fact 函数已完全消失,fact(10) 已被内联(递归)和替换(不断折叠)通过 3628800。

使用GCC - 当前版本是 2015 年 7 月的 GCC 5.2,假设您要求它进行优化(例如使用 gcc -O2g++ -O2g++ -O2-O3 编译),内联决策不容易理解。 编译器很可能会做出比你能做的更好的内联决策。有许多内部heuristics 指导它(所以没有简单的几个指导原则,但一些启发式内联,其他避免内联,可能还有一些元启发式可供选择)。了解optimize options (-finline-limit=...)、function attributes

您可以使用always_inlinegnu_inlinenoinline(以及noclone)函数属性,但我一般不建议这样做。

您可以使用 noinline 禁用内联,但通常生成的代码会更慢。所以不要那样做...

关键是编译器的优化和内联比你合理的更好,所以相信它能够很好地内联和优化。

优化编译器(另见this)可以(并且确实)内联函数,即使您不知道,例如他们有时会内联未标记为inline 的函数,或者未内联标记为inline 的某些函数。

所以不,你不想“理解这些语义和指导内联扩展的算法”,它们太难了......并且从一个编译器到另一个编译器(甚至一个版本到另一个版本)都不同。如果您真的想了解为什么 GCC 内联(这意味着要花费数月的时间,我相信您不应该为此浪费时间),请使用 -fdump-tree-all 和其他转储标志,使用 MELT 检测编译器 - 我正在开发-,深入研究源代码(因为 GCC 是 free software)。

你需要比你的一生更多的时间,或者至少几十年,来理解所有的 GCC(超过一千万行源代码)以及它是如何优化的。当你理解某些东西时,GCC 社区已经在进行新的优化等工作了......

顺便说一句,如果你编译并链接整个应用程序或库gcc -flto -O3(例如make CC='gcc -flto -O3')GCC编译器将进行链接时优化并内联一些调用accross 翻译单元(例如,在f1.c 中,您调用在f2.c 中定义的foo,而在f1.c 中对foo 的一些调用将被内联)。

编译器优化确实考虑了缓存大小(用于决定内联、展开、寄存器分配和溢出以及其他优化),尤其是在使用 gcc -mtune=native -O3 编译时

除非你强制编译器(例如,通过在 GCC 中使用 noinlinealwaysinline 函数属性,这通常是错误的并且会产生更糟糕的代码),否则你将永远无法在实践中猜测给定的代码块肯定会被内联。即使是从事 GCC 中端优化工作的人也无法可靠地猜到这一点!所以在实践中您无法可靠地理解 - 和预测 - 编译器行为,因此甚至不要浪费时间去尝试。

同时查看MILEPOST GCC;通过使用机器学习技术来调整一些 GCC 参数,他们能够有时获得惊人的性能提升,但他们肯定无法解释理解他们。

如果您在编写一些 C 或 C++ 代码时需要了解您的特定编译器,那么您的代码可能是错误的(例如,可能有一些 undefined behavior)。您应该针对某些语言规范(C11 或 C++14 标准,或特定的 GCC dialect,例如由您的 GCC 编译器记录和实现的 -std=gnu11)进行编码,并相信您的编译器会忠实于 w.r.t。那个规范。

【讨论】:

  • 虽然我从来没有说过条件返回会完全阻止内联,但您提供的示例很可能是内联的,因为递归被放在非常小的函数中。
  • GCC 有时会内联调用大型函数。
  • 完全同意。顺便说一句,MELT 是非常好的指针。谢谢。
【解决方案2】:

内联就像复制粘贴。没有太多的陷阱会阻止它工作,但应该明智地使用它。如果它失去控制,程序就会变得臃肿。

大多数编译器使用基于函数“大小”的启发式算法。由于这通常在任何代码生成通过之前,AST 节点的数量可以用作大小的代理。包含内联调用的函数需要将它们包含在自己的大小中,否则内联可能会完全失控。但是,不会生成指令的 AST 节点不应阻止内联。很难分辨什么会产生“移动”指令,什么不会产生。

由于现代 C++ 往往涉及许多在没有基础指令的情况下执行概念重新排列的函数,因此难点在于区分无指令、“仅几个”移动和足够的移动指令导致问题。判断特定实例的唯一方法是在调试器中运行程序和/或读取反汇编代码。

大多数情况下,在典型的 C++ 代码中,我们只是假设内联器已经足够努力了。对于性能关键的情况,您不能只关注它或假设任何事情都在最佳状态下工作。反汇编级别的详细性能分析至关重要。

【讨论】:

  • ……我并不是说尺寸启发式是唯一的。有关更多信息,请参阅 Basile 的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-12-28
  • 2012-03-05
  • 1970-01-01
  • 2014-07-10
  • 1970-01-01
  • 2017-06-26
  • 2021-04-29
相关资源
最近更新 更多