C语言 “内联”关键字与“内联”概念

e3bfsja2  于 2023-03-01  发布在  其他
关注(0)|答案(2)|浏览(151)

我问这个基本的问题是为了让记录更清楚。参考了this questionits currently accepted answer,这并不令人信服。但是second most voted answer给出了更好的洞察力,但也不完美。
阅读下面的内容时,请尝试区分inline * 关键字 * 和“内联”* 概念 *。
以下是我的看法:

“内联”概念

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

inline关键字

感知A

inline关键字是对编译器的一个 * 请求 *,通常用于较小的函数,以便编译器可以优化它并进行更快的调用。编译器可以随意忽略它。
我对此提出部分异议,理由如下:
1.较大的和/或递归函数不会内联,编译器完全忽略inline关键字
1.无论是否提到inline关键字,优化器都会自动内联较小的函数。
很明显,用户对使用关键字inline进行函数内联没有任何控制权。

感知B

inline与内联的概念 * 没有任何 * 关系。将inline放在大型/递归函数之前没有帮助,而较小的函数不需要它来内联。
inline的 * 唯一 * 确定性用途是维护 * 一个定义规则 *。
例如,如果一个函数是用inline声明的,那么 * 只有 * 以下内容是强制的:
1.即使在多个翻译单元中找到其主体(例如,在多个.cpp文件中包含该标头),编译器也将仅生成1个定义,并避免多个符号链接器错误。(注意:如果函数的主体不同,那么它就是未定义的行为。)

  1. inline函数的主体必须在使用它的所有翻译单元中可见/可访问。换句话说,在.h中声明inline函数并在 * 任何一个 * .cpp文件中定义将导致其他.cpp文件出现“未定义符号链接器错误
    判决
    我觉得“A”是完全错误的,“B”是完全正确的。
    有一些引用标准对此,但我期待一个答案,逻辑上解释,如果这个判决正确与否。
    Bjarne Stroustrup的电子邮件回复:
  • “几十年来,人们一直承诺编译器/优化器在内联方面比人类更好,或者很快就会更好。这在理论上可能是正确的,但对于优秀的程序员来说,这还没有付诸实践,特别是在整个程序优化不可行的环境中。明智地使用显式内联会有很大的好处。"*
3z6pesqy

3z6pesqy1#

我不确定你的说法:
无论是否提到内联,优化器都会自动“内联”较小的函数......很明显,用户无法使用关键字inline控制函数“内联”。
我听说编译器可以忽略您的inline请求,但我不认为他们完全忽略了它。
我查看了Clang和LLVM的Github存储库来找出答案。(谢谢,开源软件!)我发现**inline关键字 * 确实 * 使Clang/LLVM更有可能内联函数。

寻找

the Clang repository中搜索单词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切换到the LLVM repository
搜索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,我们就提高它。(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()计算的InlineCostThreshold值。最后,我们返回一个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只是一个提示,其他因素可能会超过它。

cgyqldqp

cgyqldqp2#

两者都是正确的。
inline的使用可能会也可能不会影响编译器内联任何特定函数调用的决定,所以A是正确的--它充当了内联函数调用的非绑定请求,编译器可以自由地忽略它。
inline的语义效果是放松了一个定义规则的限制,允许在多个翻译单元中使用相同的定义,如B中所述。对于许多编译器来说,这是允许内联函数调用所必需的-定义必须在此时可用,并且编译器一次只需要处理一个翻译单元。

相关问题