38

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

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

这是我的看法:

内联概念

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

inline关键字_

感知A

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

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

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

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

知觉乙

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

唯一确定性用途inline是维护One Definition Rule

即,如果声明了一个函数,inline那么只有以下内容是强制性的:

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

判决

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

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

4

2 回答 2

58

我不确定您的说法:

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

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

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

搜索

inlineClang 存储库中搜索单词会导致令牌说明符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说明符被转换为与语言无关的 LLVMFunction对象的属性。我们从 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;

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

getInlineThreshold()似乎只有一个呼叫站点,是以下成员SimpleInliner

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

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

搜索::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()是在对象上设置CostThreshold成员变量CallAnalyzer。返回值仅表示某些其他因素覆盖了成本与阈值分析的情况,如下所示:

// 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();

否则,我们返回一个存储Costand的对象Threshold

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

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

真正的决定

它位于 bool Inliner::shouldInline(CallSite CS). 另一个大功能。它在一开始就调用getInlineCost()

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

在整个方法中,都会调用InlineCost::costDelta()- 将使用s计算的InlineCosts值。最后,我们返回一个. 做出决定。在:ThresholdanalyzeCall()boolInliner::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()的决定进行内联。

所以Thresholdinline关键字影响,最后用来决定是否内联。

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

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

于 2014-11-20T16:41:49.810 回答
21

两者都是正确的。

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

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

于 2014-11-20T15:18:58.740 回答