C语言 “inline”关键字和“inlining”优化是独立的概念吗?

rsl1atfo  于 2023-10-16  发布在  其他
关注(0)|答案(3)|浏览(116)

我问这个基本问题是为了把记录弄清楚。已经提到了this questionits currently accepted answer,这是没有说服力的。然而,second most voted answer提供了更好的洞察力,但也不是完美的。
在阅读下面的内容时,请尝试区分inline * 关键字 * 和“内联”* 概念 *。
以下是我的观点:

inlining概念

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

inline关键字

感知A

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

感知B

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

  1. inline函数的主体必须在所有使用它的翻译单元中可见/可访问。换句话说,在.h中声明inline函数并在 * 任何一个 * .cpp文件中定义将导致其他.cpp文件出现“未定义符号链接器错误

判决

IMO,感知“A”是完全错误的,感知“B”是完全正确的。
有一些报价标准,但我期待一个答案,从逻辑上解释,如果这个判决正确与否。
来自Bjarne Stroustrup的电子邮件回复:
“几十年来,人们一直承诺编译器/优化器在内联方面已经或即将优于人类。这在理论上可能是正确的,但对于优秀的程序员来说,这在实践中仍然是不可能的,特别是在整个程序优化不可行的环境中。明智地使用显式内联会带来很大的好处。"*

qvsjd97n

qvsjd97n1#

我不确定你的说法:
较小的函数会被优化器自动“内联”,而不管是否提到内联。很明显,使用关键字inline,用户对函数“内联”没有任何控制权。
我听说编译器可以随意忽略你的inline请求,但我不认为他们会完全忽略它。
我查看了Github存储库中的Clang和LLVM。(谢谢开源软件!))我发现**inline关键字 * 确实 * 让Clang/LLVM更有可能内联函数。**

搜索

the Clang repository中搜索单词inline会找到标记说明符kw_inline。看起来Clang使用了一个聪明的基于宏的系统来构建lexer和其他关键字相关的函数,所以没有像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);

Clang到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,我们就把它提高。* (提示阈值可从命令行设置。)*
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行,并做了决定函数是否可内联的真实的实质性工作。它权衡了许多因素,但当我们阅读该方法时,我们看到它的所有计算都操纵ThresholdCost。最后:

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关键字的影响,并在最后决定是否内联。
因此,您的Perception B部分是错误的,因为至少有一个主要的编译器根据inline关键字更改了其优化行为。
但是,我们也可以看到inline只是一个提示,其他因素可能会超过它。

vshtjzan

vshtjzan2#

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

mbjcgjjk

mbjcgjjk3#

我只是想发布一个inline影响内联的概念验证示例。
Here it is

namespace {
struct S {
    auto foo(unsigned int x) {
        ++x; x *= x;
        ++x; x *= x;
        ++x; x *= x;
        ++x; x *= x;
        ++x; x *= x;
        ++x; x *= x;
        ++x; x *= x;
        ++x; x *= x;
        return x;
    }
    inline auto bar(unsigned int x) {
        ++x; x *= foo(x);
        ++x; x *= foo(x);
        ++x; x *= foo(x);
        ++x; x *= foo(x);
        ++x; x *= foo(x);
        return x;
    }
    auto baz(unsigned int x) {
        ++x; x *= bar(x);
        ++x; x *= bar(x);
        ++x; x *= bar(x);
        ++x; x *= bar(x);
        return x;
    }
};
}
int main(int argc, char *argv[]) {
    return S().baz(argc);
}

即使使用-O3,Clang 16.0.0也不会内联bar;在这种情况下,您将在输出中看到call指令。但是如果您将inline添加到bar,则这些将完全内联,并且call指令将消失。* 尽管所有方法都是语义隐式内联的!*
这个例子显然是人为设计的,但我遇到过实际情况,在这种情况下,它会导致不小的性能差异。

相关问题