【问题标题】:"inline" keyword vs "inlining" concept“内联”关键字与“内联”概念
【发布时间】:2015-01-18 12:16:16
【问题描述】:

我问这个基本问题是为了让记录变得直截了当。提到了this questionits currently accepted answer,没有说服力。然而second most voted answer 提供了更好的洞察力,但也不是完美的。

阅读下面的内容时,请区分inline关键字和“内联”概念

这是我的看法:

内联概念

这样做是为了节省函数的调用开销。它更类似于宏样式的代码替换。没有什么可争论的。

inline 关键字

感知 A

inline 关键字是对通常用于较小函数的编译器的请求,以便编译器可以对其进行优化并进行更快的调用。编译器可以随意忽略它。

我对此提出异议,原因如下:

  1. 较大的递归函数没有内联,编译器会忽略 inline 关键字。
  2. 无论是否提及 inline 关键字,优化器都会自动内联较小的函数。

很明显,用户无法通过使用关键字inline 来控制函数内联。

感知 B

inline 与内联的概念无关。将 inline 放在大/递归函数之前无济于事,较小的函数也不需要它,因为内联。

唯一确定性地使用inline 是为了维护One 定义规则.

即如果一个函数是用inline 声明的,那么只有下面的东西是强制性的:

  1. 即使在多个翻译单元中找到它的主体(例如,在多个 .cpp 文件中包含该标头),编译器也只会生成 1 个定义并避免多符号链接器错误。 (注意:如果该函数的主体不同,则为未定义行为。)
  2. inline 函数的主体必须在所有使用它的翻译单元中可见/可访问。换句话说,在.h 中声明inline 函数并在任何一个 .cpp 文件中定义将导致其他.cpp 文件出现“未定义符号链接器错误”

判决

“A”的看法完全错误,而“B”的看法完全正确

对此有一些标准引用,但是我期待一个能从逻辑上解释这个判断是真还是假的答案。

【问题讨论】:

  • "Wrong" 有点苛刻,你不觉得吗。 “A”视角是给 C 程序员学习 C++ 的标准解释。它本身并没有错,它只是不完整。例如,它没有提及有关多个声明和定义的规则如何变化。结合这两种观点,你有一个更完整的画面
  • 对于某些编译器,inline 关键字提醒编译器在 debug 模式下或在关闭优化时内联函数。对于更高级别的优化,无论是否使用inline 关键字,编译器都可以内联小函数。请记住,inline 关键字也可以与 freestanding 函数一起使用。我在 IAR EWARM 编译器上有这个概念。
  • 这个问题的基本前提存在问题,即语言定义/标准——以及因此基于语言律师游戏的任何答案——与 what 绝对没有直接联系编译器实际上会这样做。例如“优化器会自动“内联”较小的函数”->什么优化器?如果我使用 TCC 或 -O0 会怎样?如果该功能尚未在我的平台上完成怎么办?如果我使用的是悲观编译器(认为这是之前讨论过的)怎么办?语言没有定义这些东西。感知 A 完全依赖于实现。
  • 我认为这是关于内联的很好读物:drdobbs.com/inline-redux/184403879
  • @MooingDuck:有个问题;它只是没有明显地标有问号。最后一句话说他想知道“这个判决是真是假”。这是一种迂回的方式来询问“我得出了这个结论;它是否正确?”

标签: c++ c language-lawyer inline-functions


【解决方案1】:

我不确定您的声明:

无论是否提及内联,优化器都会自动“内联”较小的函数... 很明显,用户无法使用关键字inline 对函数“内联”进行任何控制。

我听说编译器可以随意忽略您的 inline 请求,但我认为他们并没有完全无视它。

我查看了 Clang 和 LLVM 的 Github 存储库以找出答案。 (感谢开源软件!)我发现inline 关键字确实使 Clang/LLVM 更有可能内联函数。

搜索

the Clang repository 中搜索单词inline 会导致令牌说明符kw_inline。看起来 Clang 使用了一个聪明的基于宏的系统来构建词法分析器和其他与关键字相关的函数,因此可以找到像 if (tokenString == "inline") return kw_inline 这样的直接注释。但是Here in ParseDecl.cpp,我们看到kw_inline 导致调用DeclSpec::setFunctionSpecInline()

case tok::kw_inline:
  isInvalid = DS.setFunctionSpecInline(Loc, PrevSpec, DiagID);
  break;

Inside that function,如果是重复的inline,我们会设置一个位并发出警告:

if (FS_inline_specified) {
  DiagID = diag::warn_duplicate_declspec;
  PrevSpec = "inline";
  return true;
}
FS_inline_specified = true;
FS_inlineLoc = Loc;
return false;

在别处搜索FS_inline_specified,我们看到它是位域中的一个位,以及it's used in a getter functionisInlineSpecified()

bool isInlineSpecified() const {
  return FS_inline_specified | FS_forceinline_specified;
}

搜索isInlineSpecified()的调用点,我们找到the codegen,我们将C++解析树转换为LLVM中间表示:

if (!CGM.getCodeGenOpts().NoInline) {
  for (auto RI : FD->redecls())
    if (RI->isInlineSpecified()) {
      Fn->addFnAttr(llvm::Attribute::InlineHint);
      break;
    }
} else if (!FD->hasAttr<AlwaysInlineAttr>())
  Fn->addFnAttr(llvm::Attribute::NoInline);

Clang 到 LLVM

我们已经完成了 C++ 解析阶段。现在,我们的 inline 说明符被转换为与语言无关的 LLVM Function 对象的属性。我们从 Clang 切换到 the LLVM repository

正在搜索llvm::Attribute::InlineHint yields the method Inliner::getInlineThreshold(CallSite CS) (带有看起来很吓人的无括号if 块)

// Listen to the inlinehint attribute when it would increase the threshold
// and the caller does not need to minimize its size.
Function *Callee = CS.getCalledFunction();
bool InlineHint = Callee && !Callee->isDeclaration() &&
  Callee->getAttributes().hasAttribute(AttributeSet::FunctionIndex,
                                       Attribute::InlineHint);
if (InlineHint && HintThreshold > thres
    && !Caller->getAttributes().hasAttribute(AttributeSet::FunctionIndex,
                                             Attribute::MinSize))
  thres = HintThreshold;

所以我们已经从优化级别和其他因素中获得了一个基线内联阈值,但如果它低于全局HintThreshold,我们会提高它。 (HintThreshold 可从命令行设置。)

getInlineThreshold() 似乎只有one call siteSimpleInliner 的成员:

InlineCost getInlineCost(CallSite CS) override {
  return ICA->getInlineCost(CS, getInlineThreshold(CS));
}

它在其指向InlineCostAnalysis 实例的成员指针上调用一个虚拟方法,也称为getInlineCost

搜索::getInlineCost() 以查找属于类成员的版本,我们找到一个属于AlwaysInline 的版本 - 这是一种非标准但广泛支持的编译器功能 - 另一个属于InlineCostAnalysis 的成员。它使用它的Threshold 参数here

CallAnalyzer CA(Callee->getDataLayout(), *TTI, AT, *Callee, Threshold);
bool ShouldInline = CA.analyzeCall(CS);

CallAnalyzer::analyzeCall() 超过 200 行,does the real nitty gritty work of deciding if the function is inlineable。它权衡了许多因素,但是当我们阅读该方法时,我们看到它的所有计算都在操纵ThresholdCost。最后:

return Cost < Threshold;

但是名为ShouldInline 的返回值确实是用词不当。其实analyzeCall()的主要目的是在CallAnalyzer对象上设置CostThreshold成员变量。返回值仅表示某些其他因素已覆盖成本与阈值分析的情况,as we see here

// Check if there was a reason to force inlining or no inlining.
if (!ShouldInline && CA.getCost() < CA.getThreshold())
  return InlineCost::getNever();
if (ShouldInline && CA.getCost() >= CA.getThreshold())
  return InlineCost::getAlways();

否则,我们返回一个存储CostThreshold 的对象。

return llvm::InlineCost::get(CA.getCost(), CA.getThreshold());

因此,在大多数情况下,我们不会返回是或否的决定。搜索继续!这个getInlineCost()的返回值在哪里使用?

真正的决定

It's found inbool Inliner::shouldInline(CallSite CS)。另一个大功能。它一开始就调用getInlineCost()

事实证明,getInlineCost 分析了内联函数的内在成本 - 它的参数签名、代码长度、递归、分支、链接等 - 以及一些关于 的汇总信息每个 使用该功能的地方。另一方面,shouldInline() 将此信息与更多关于特定使用该功能的地方的数据结合起来。

在整个方法中都会调用InlineCost::costDelta() - 这将使用analyzeCall() 计算的InlineCosts Threshold 值。最后,我们返回一个bool。做出决定。在Inliner::runOnSCC()

if (!shouldInline(CS)) {
  emitOptimizationRemarkMissed(CallerCtx, DEBUG_TYPE, *Caller, DLoc,
                               Twine(Callee->getName() +
                                     " will not be inlined into " +
                                     Caller->getName()));
  continue;
}

// Attempt to inline the function.
if (!InlineCallIfPossible(CS, InlineInfo, InlinedArrayAllocas,
                          InlineHistoryID, InsertLifetime, DL)) {
  emitOptimizationRemarkMissed(CallerCtx, DEBUG_TYPE, *Caller, DLoc,
                               Twine(Callee->getName() +
                                     " will not be inlined into " +
                                     Caller->getName()));
  continue;
}
++NumInlined;

InlineCallIfPossible() 根据shouldInline() 的决定进行内联。

所以Threshold受到inline关键字的影响,最后用来决定是否内联。

因此,您的 Perception B 部分错误,因为至少有一个主要编译器根据 inline 关键字更改了其优化行为。

但是,我们也可以看到inline只是一个提示,其他因素可能会超过它。

【讨论】:

  • Bjarne Stroustrup 的评论:“几十年来,人们一直承诺编译器/优化器在内联方面已经或将很快比人类更好。这在理论上可能是正确的,但它仍然不是'对于优秀的程序员来说实际上并不可行,尤其是在整个程序优化不可行的环境中。明智地使用显式内联可以带来重大收益。”
  • 是的。始终检查汇编器输出中的性能关键代码。编译器通常会做正确的事情,但并非总是如此。 GCC 和 Clang 有 __attribute__(always_inline) 和 MSVC 有 __forceinline 但即使是那些也可能因为 some functions are not inlineable 而失败。
  • 感谢您的赏金!了解有关 LLVM 内部的更多信息很有趣。
  • @iammilind:我不确定“静态”编译器如何比人类更了解如何优化事物,除非代码被注释以指示各种事情发生的频率。如果一个更改会使程序在处理某些文件时快 10%,而在处理其他文件时减慢 50%,则不可能期望编译器在不知道程序将花费更多文件的情况下知道该更改是好是坏时间紧迫。动态编译器 (JIT) 可能能够使用执行模式即时做出此类决定,但是...
  • ...静态编译器没有这种能力。在可以引导静态编译器针对适当情况进行优化的情况下,静态编译器通常可以胜过动态编译器,但该过程通常需要了解未来编译器无法预测的情况。
【解决方案2】:

两者都是正确的。

inline 的使用可能会或可能不会影响编译器内联对函数的任何特定调用的决定。所以 A 是正确的 - 它充当一个非绑定请求,调用内联函数,编译器可以随意忽略。

inline 的语义效果是放宽单一定义规则的限制,以允许在多个翻译单元中进行相同的定义,如 B 中所述。对于许多编译器,这是允许函数调用内联的必要条件 -定义必须在那个时候可用,并且编译器一次只需要处理一个翻译单元。

【讨论】:

  • 当然,还有 LTO/whole-program-optimization,它不依赖于与使用在同一个 TU 中的定义。
  • @Deduplicator:确实,这就是为什么我用“对于许多编译器”来限定它。我考虑过添加其他方案的简要说明,但这超出了一个简单问题的范围。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多