"inline"关键字与"inlining"概念

"inline" keyword vs "inlining" concept

本文关键字:概念 inlining 关键字 inline      更新时间:2023-10-16

我问这个基本问题是为了澄清记录。已经提到了这个问题及其目前接受的答案,这并不令人信服。然而,第二多投票的答案提供了更好的洞察力,但也不完美。

在阅读以下内容时,请尝试区分inline关键字和"内联"概念

这是我的看法:

"内联"概念

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

inline关键字

感知 A

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

出于以下原因,我对此提出部分异议:

  1. 较大的和/或递归函数无论如何都不会内联,编译器完全忽略inline关键字
  2. 较小的函数由优化器自动内联,无论是否提及inline关键字。

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

感知 B

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

inline的唯一确定性用途是维护定义规则

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

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

判决

IMO,感知"A"是完全

错误的,感知"B"是完全正确的

对此

有一些引用,但是我期待一个答案,从逻辑上解释这个判决是否正确。

<小时 />

Bjarne Stroustrup的电子邮件回复:

"几十年来,人们一直承诺编译器/优化器在内联方面已经或很快就会比人类更好。这在理论上可能是正确的,但对于优秀的程序员来说仍然不是这样,尤其是在全程序优化不可行的环境中。明智地使用显式内联会带来重大收益。

我不确定你的说法:

较小的函数由优化器自动"内联",无论是否提及内联...... 很明显,用户无法使用关键字"内联"来控制功能inline

我听说编译器可以自由地忽略您的inline请求,但我不认为他们完全无视它。

我浏览了Github存储库,寻找Clang和LLVM。 (谢谢,开源软件! 我发现 The inline 关键字确实使 Clang/LLVM 更有可能内联函数。

搜索

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

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

在该函数中,我们设置了一个位,如果它是一个重复的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,我们看到它是位域中的单个位,并且用于 getter 函数,isInlineSpecified()

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

搜索 isInlineSpecified() 的调用站点,我们找到 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);

叮当到LLVM

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

搜索llvm::Attribute::InlineHint会产生方法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,我们会提高它。 (提示阈值可从命令行设置。

getInlineThreshold()似乎只有一个呼叫站点,SimpleInliner的成员:

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

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

搜索::getInlineCost()以查找作为类成员的版本,我们发现一个是AlwaysInline的成员 - 这是一个非标准但广泛支持的编译器功能 - 另一个是InlineCostAnalysis的成员。 它在此处使用其Threshold参数:

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

CallAnalyzer::analyzeCall() 超过 200 行,并执行确定函数是否可内联的真正细节工作。 它权衡了许多因素,但是当我们通读该方法时,我们看到它的所有计算要么操纵Threshold要么操纵Cost 。 最后:

return Cost < Threshold;

但是名为 ShouldInline 的返回值确实用词不当。 实际上,analyzeCall()的主要目的是在CallAnalyzer对象上设置CostThreshold成员变量。 返回值仅指示其他因素覆盖成本与阈值分析的情况,如下所示:

// 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()的返回值在哪里使用?

真正的决定

它在bool Inliner::shouldInline(CallSite CS)中找到。 另一个大功能。 它一开始就getInlineCost()

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

在整个方法中,有对InlineCost::costDelta()的调用 - 它将使用由 analyzeCall() 计算的 InlineCost s 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关键字的影响,最终用于决定是否内联。

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

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

两者都是正确的。

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

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