【问题标题】:Why should I ever use inline code?为什么我应该使用内联代码?
【发布时间】:2010-09-13 01:29:32
【问题描述】:

我是一名 C/C++ 开发人员,这里有几个问题一直困扰着我。

  • “常规”代码和内联代码有很大区别吗?
  • 主要区别是什么?
  • 内联代码只是宏的“形式”吗?
  • 在选择内联代码时必须做什么样的权衡?

谢谢

【问题讨论】:

    标签: c++ optimization inline-functions tradeoff


    【解决方案1】:

    性能

    正如在之前的答案中所建议的,使用 inline 关键字可以通过内联函数调用使代码更快,通常以增加可执行文件为代价。 “内联函数调用”只是意味着在相应地填写参数后,用函数的实际代码替换对目标函数的调用。

    然而,现代编译器非常擅长在设置为高度优化时自动内联函数调用无需用户提示。实际上,编译器通常更好在确定什么调用内联以获得速度增益。

    为了提高性能而显式声明函数inline(几乎?)总是不必要的!

    此外,编译器可以并将 忽略 inline 请求(如果它适合他们)。如果对函数的调用不可能内联(即使用非平凡递归或函数指针),而且如果函数太大而无法获得有意义的性能提升,编译器就会这样做。

    一个定义规则

    但是,使用 inline 关键字 has other effects 声明内联函数,实际上可能需要满足单一定义规则 (ODR):C++ 标准中的这条规则规定给定的符号可以声明多次,但只能定义一次。如果链接编辑器(=链接器)遇到几个相同的符号定义,就会产生错误。

    解决此问题的一种方法是通过声明 static 来提供内部链接来确保编译单元不会导出给定符号。

    但是,通常最好将函数标记为inline。这告诉链接器将此函数的所有定义跨编译单元合并为一个定义,具有一个地址和共享的函数静态变量。

    例如,考虑以下程序:

    // header.hpp
    #ifndef HEADER_HPP
    #define HEADER_HPP
    
    #include <cmath>
    #include <numeric>
    #include <vector>
    
    using vec = std::vector<double>;
    
    /*inline*/ double mean(vec const& sample) {
        return std::accumulate(begin(sample), end(sample), 0.0) / sample.size();
    }
    
    #endif // !defined(HEADER_HPP)
    
    // test.cpp
    #include "header.hpp"
    
    #include <iostream>
    #include <iomanip>
    
    void print_mean(vec const& sample) {
        std::cout << "Sample with x̂ = " << mean(sample) << '\n';
    }
    
    // main.cpp
    #include "header.hpp"
    
    void print_mean(vec const&); // Forward declaration.
    
    int main() {
        vec x{4, 3, 5, 4, 5, 5, 6, 3, 8, 6, 8, 3, 1, 7};
        print_mean(x);
    }
    

    请注意,两个.cpp 文件都包含头文件,因此也包含mean 的函数定义。尽管保存文件时使用了包含防止双重包含的保护措施,但这将导致同一函数的两个定义,尽管在不同的编译单元中。

    现在,如果您尝试链接这两个编译单元 - 例如使用以下命令:

    ⟩⟩⟩ g++ -std=c++11 -pedantic main.cpp test.cpp
    

    您将收到一条错误消息,提示“重复符号 __Z4meanRKNSt3__16vectorIdNS_9allocatorIdEEEE”(这是我们函数 meanmangled name)。

    但是,如果您取消注释函数定义前面的 inline 修饰符,代码将正确编译和链接。

    函数模板是一种特殊情况:它们总是内联,无论它们是否以这种方式声明。这并不意味着编译器会内联对它们的调用,但它们不会违反 ODR。对于在类或结构中定义的成员函数也是如此。

    【讨论】:

    • Dang,我一直在寻找这样的修复方法......我有一个带有函数定义+实现的标题,但没有内联。虽然有header guards,但还是有多个定义……终于找到了解决办法:)
    • 我觉得 应该 是处理模板函数定义比使用说明符 staticinline 更好的方法(不是我能想到的'不要限制你的函数可以使用的类型数量)......这两种情况不会导致更大的二进制文件吗?此外,如果编译器“选择忽略提示”因为您的模板函数是 25 行代码,那么使用 inline 关键字如何发挥作用?结果是否与static 说明符相同?为什么inlinestatic更好
    • @Assimilater 它不会增加可执行文件的大小,因为现代编译器/链接器将确保没有多余的定义。另外,当我说编译器可以忽略inline 请求时,这只是为了优化。编译器仍必须确保满足 ODR(如何做到这一点取决于编译器,并不意味着函数调用实际上是内联的;它只是意味着编译器会跟踪多个定义并合并它们)。最后,我的示例选择不当,因为函数模板始终是内联的。
    • @Assimilater 简而言之,标题中的using type aliases 完全没问题,您可能会将其与using namespace declarations 混淆,后者并不好。关于staticinline 之间的区别:static 定义仅在编译单元内可见(内部链接),如果您有具有内部链接的重复函数,链接器将合并这些.这意味着static,而不是inline,将创建重复代码。
    • @Assimilater 此外,正如您自己发现的那样,在 static 函数中,函数静态变量将引用不同的实体,而在 inline 函数中,函数静态变量将引用相同的实体跨翻译单元的实体。请参阅此示例:gist.github.com/klmr/9e06284a1ca9152bee2bfc3b1df14003。一般来说,在 C++ 中使用 static 函数的理由很少。
    【解决方案2】:
    • “常规”代码和内联代码有很大区别吗?

    是和不是。不,因为内联函数或方法与常规函数或方法具有完全相同的特征,最重要的是它们都是类型安全的。是的,因为编译器生成的汇编代码会有所不同;对于常规函数,每次调用都将转换为几个步骤:将参数压入堆栈,跳转到函数,弹出参数等,而对内联函数的调用将被其实际代码替换,例如宏。

    • 内联代码只是宏的“形式”吗?

    !宏是简单的文本替换,可能会导致严重错误。考虑以下代码:

    #define unsafe(i) ( (i) >= 0 ? (i) : -(i) )
    
    [...]
    unsafe(x++); // x is incremented twice!
    unsafe(f()); // f() is called twice!
    [...]
    

    使用内联函数,您可以确定参数会在函数实际执行之前进行评估。它们还将进行类型检查,并最终转换为与形式参数类型匹配。

    • 在选择内联代码时必须做什么样的权衡?

    通常,使用内联函数时程序执行应该更快,但二进制代码更大。更多信息,请阅读GoTW#33

    【讨论】:

    • 值得一提的是,程序员只能提示编译器使用内联代码由编译器实际做出选择(即使在类中定义了方法)。只有在分析表明这样做有优势时,编译器才会内联
    • 我的理解是你甚至不能再提示编译器了。你可以随便说inline int f() { ... },就优化而言,编译器会看到它等同于int f () { ... },然后运行自己的分析来确定是否内联。
    • 现代编译器将其视为一个相当强的提示,但它们也会在您没有特别要求的地方内联函数。当您遇到 L1 icache 的大小时,这是非常有问题的,因为内联实际上可以显着减慢速度……
    【解决方案3】:

    内联代码本质上与宏类似,但它是实际的真实代码,可以进行优化。非常小的函数通常适合内联,因为与方法所做的少量实际工作相比,设置函数调用(将参数加载到适当的寄存器中)所需的工作成本很高。使用内联,无需设置函数调用,因为代码直接“粘贴”到任何使用它的方法中。

    内联会增加代码大小,这是它的主要缺点。如果代码太大以至于无法放入 CPU 缓存中,则可能会严重降低速度。您只需要在极少数情况下担心这一点,因为您不太可能在很多地方使用方法,增加的代码会导致问题。

    总之,内联非常适合加速被多次调用但不会在太多地方调用的小方法(不过,100 个地方仍然可以 - 您需要进入非常极端的示例才能获得任何显着的代码膨胀)。

    编辑:正如其他人指出的那样,内联只是对编译器的建议。如果它认为你在发出愚蠢的请求,比如内联一个巨大的 25 行方法,它可以随意忽略你。

    【讨论】:

    • 请记住,内联函数保证与等效的非内联函数具有相同的语义。宏可能会产生意想不到的副作用。
    • 内联不一定会增加代码大小。例如。内联的简单算术运算可能比通过函数调用实现的要小。
    【解决方案4】:
    • “常规”代码和内联代码有很大区别吗?

    是的 - 内联代码不涉及函数调用,并将寄存器变量保存到堆栈中。每次“调用”它都会使用程序空间。因此,总体而言,执行所需的时间更少,因为处理器中没有分支,也没有保存状态、清除缓存等。

    • 内联代码只是宏的“形式”吗?

    宏和内联代码有相似之处。最大的区别是内联代码被专门格式化为一个函数,因此编译器和未来的维护者有更多的选择。具体来说,如果你告诉编译器优化代码空间,或者未来的维护者最终扩展它并在他们的代码中的许多地方使用它,它可以很容易地变成一个函数。

    • 选择内联代码时必须做什么样的权衡?

      • 宏:代码空间使用率高,执行速度快,如果“函数”很长,则难以维护
      • 功能:代码空间占用少,执行速度慢,易于维护
      • 内联函数:代码空间占用高、执行速度快、易于维护

    需要注意的是,寄存器保存和跳转到函数确实会占用代码空间,所以对于非常小的函数,内联可以比函数占用更少的空间。 em>

    -亚当

    【讨论】:

    • 另外值得注意的是,在函数的许多(或全部)参数是常量的情况下,inline 有时会比函数调用占用 很多 的空间。例如,给定inline double distsquared(double dx, double dy, double dz) { return sqrt(x*x+y*y+z*z); },代码d=distsquared(1.0, 2.0, z); 可以扩展为d=z*z+5;,这可能比推送三个double 参数然后调用函数的代码更紧凑。
    【解决方案5】:

    这取决于编译器...
    假设你有一个愚蠢的编译器。通过指示必须内联函数,它会在每次调用时放置函数内容的副本。

    优点:没有函数调用开销(放参数、推送当前PC、跳转到函数等)。例如,在大循环的中心部分可能很重要。

    不便之处:膨胀生成的二进制文件。

    它是一个宏吗?不是真的,因为编译器仍然会检查参数的类型等。

    智能编译器呢?如果他们“感觉”函数太复杂/太大,他们可以忽略 inline 指令。也许他们可以自动内联一些琐碎的函数,比如简单的 getter/setter。

    【讨论】:

      【解决方案6】:

      内联与宏的不同之处在于它是对编译器的提示(编译器可能决定不内联代码!)并且宏是在编译之前生成的源代码文本,因此被“强制”内联。

      【讨论】:

        【解决方案7】:

        将函数标记为内联意味着编译器具有选项以包含在调用它的“内联”中,如果编译器选择这样做的话;相比之下,宏将总是就地展开。内联函数将设置适当的调试符号,以允许符号调试器跟踪它的来源,而调试宏则令人困惑。内联函数必须是有效函数,而宏......好吧,不要。

        决定将函数声明为内联在很大程度上是一种空间折衷——如果编译器决定内联它,您的程序将会更大(特别是如果它不是静态的,在这种情况下,至少需要一个非内联副本供任何外部对象使用);事实上,如果函数很大,这可能会导致性能下降,因为缓存中的代码较少。然而,一般性能提升只是您摆脱了函数调用本身的开销。对于作为内部循环的一部分调用的小函数,这是一个有意义的权衡。

        如果您信任您的编译器,请随意标记内部循环中使用的小函数inline;编译器将负责在决定是否内联时做正确的事情。

        【讨论】:

          【解决方案8】:

          如果您在 f.e. 中将代码标记为内联C++ 你也告诉你的编译器代码应该内联执行,即。该代码块将“或多或少”插入到调用它的位置(从而消除堆栈上的推送、弹出和跳转)。所以,是的……如果函数适合这种行为,建议使用。

          【讨论】:

            【解决方案9】:

            “inline”类似于 2000 年的“register”。不用担心,编译器可以比您更好地决定要优化的内容。

            【讨论】:

            • 不准确——如果你没有将函数标记为内联,则编译器不允许在未经你批准的情况下内联它。因此,通过使用“内联”,您可以为编译器提供更多选择,使其做出优于程序员的决策。
            • as-if 规则允许编译器根据需要内联函数。但是,只有在函数定义可见时才能这样做。由于 ODR,这基本上意味着编译器不能隐式内联来自不同 TU 的函数。
            • @Charles,不正确。允许编译器做任何不改变程序语义的事情。即使您没有将它们声明为内联,所有现代编译器都会内联琐碎函数。
            • 确切地说:@Nils:这取决于编译器设置。使用 Visual Studio 可以实现三度(无内联,仅内联,任何合适的)。 @Richard Corden - 整个程序优化甚至可以跨 TU 内联。
            • @Suma:理论上是这样。有多少平台/编译器支持 WPO?我需要支持的平台仍然使用非常基本的链接器来完成这项工作。其他人强调了一些链接器如何仍然不删除重复的模板实例化。 'inline' 还会出现一段时间。
            【解决方案10】:

            通过内联,编译器在调用点插入函数的实现。 您正在做的是消除函数调用开销。 但是,不能保证您的所有内联候选者实际上都会被编译器内联。但是,对于较小的函数,编译器总是内联的。 因此,如果您有一个被多次调用但只有有限数量的代码(几行代码)的函数,您可以从内联中受益,因为函数调用开销可能比函数本身的执行花费更长的时间。

            一个很好的内联候选的典型例子是简单的具体类的getter。

            CPoint
            {
              public:
            
                inline int x() const { return m_x ; }
                inline int y() const { return m_y ; }
            
              private:
                int m_x ;
                int m_y ;
            
            };
            

            一些编译器(例如 VC2005)有一个积极内联的选项,使用该选项时您不需要指定 'inline' 关键字。

            【讨论】:

              【解决方案11】:

              我不会重复上面的内容,但值得注意的是,虚函数不会被内联,因为调用的函数是在运行时解析的。

              【讨论】:

              • 如果编译器在调用点知道对象的动态类型,则可以内联虚拟函数。例如,如果你有一个基类的指针或引用,它不会被内联,如果你有一个派生对象(不是 ptr/ref),它可能是内联的。
              【解决方案12】:

              内联通常在优化级别 3 启用(在 GCC 的情况下为 -O3)。在某些情况下(如果可能),这可能会显着提高速度。

              程序中的显式内联可以增加一些速度,但会增加代码大小。

              您应该看看哪个是合适的:代码大小或速度,并决定是否应该将其包含在您的程序中。

              您可以只打开第 3 级优化而忘记它,让编译器完成他的工作。

              【讨论】:

                【解决方案13】:

                是否应该内联的答案归结为速度。 如果你在一个紧密的循环中调用一个函数,而且它不是一个超级大的函数,而是一个在调用函数上浪费了很多时间的函数,那么让这个函数内联,你会得到很多好处你的钱。

                【讨论】:

                  【解决方案14】:

                  首先,内联是对编译器的内联函数的请求。所以编译器是否内联。

                  1. 何时使用?何时使用函数 很少的行(适用于所有访问者 和 mutator) 但不适用于递归 功能
                  2. 优势?不涉及调用函数调用所花费的时间
                  3. 编译器是否内联了它自己的任何函数?是的,当函数在类的头文件中定义时

                  【讨论】:

                    【解决方案15】:

                    内联是一种提高速度的技术。但是在你的情况下使用探查器来测试它。我发现 (MSVC) 内联并不总是可以交付,当然也不会以任何壮观的方式交付。运行时间有时会减少几个百分点,但在稍有不同的情况下会增加几个百分点。

                    如果代码运行缓慢,请使用分析器查找故障点并解决这些问题。

                    我已经停止在头文件中添加内联函数,它增加了耦合但回报很少。

                    【讨论】:

                      【解决方案16】:

                      内联代码更快。无需执行函数调用(每个函数调用都需要一些时间)。缺点是您不能将指针传递给内联函数,因为该函数并不真正作为函数存在,因此没有指针。此外,该函数不能导出到公共(例如,库中的内联函数在链接到库的二进制文件中不可用)。另一个问题是,如果您从不同的地方调用函数,二进制文件中的代码部分将会增长(因为每次生成函数的副本而不是只有一个副本并总是跳到那里)

                      通常您不必手动决定是否内联函数。例如。 GCC 将根据优化级别 (-Ox) 和其他参数自动决定。它将考虑诸如“功能有多大?”之类的事情。 (指令数量),在代码中调用它的频率,通过内联二进制文件会变大多少,以及其他一些指标。例如。如果一个函数是静态的(因此无论如何都不会导出)并且只在您的代码中调用一次并且您从不使用指向该函数的指针,那么 GCC 很可能会决定自动内联它,因为它不会产生负面影响(二进制只内联一次不会变大)。

                      【讨论】:

                      • 一些更正。您可以将指针传递给内联函数。该函数可以是“extern”,但它的定义需要对使用它的每个 TU 可见。代码可能增长。 AFAIK,GCC 还不能进行完全隐式内联所需的全程序优化。
                      • GCC 的解决方法是将一组 TU 的所有代码集中在一起。它有几个脚本。 KDE 做到了。缺点是在 KDE 编译期间使用 800+ MB RAM。
                      猜你喜欢
                      • 1970-01-01
                      • 2011-01-30
                      • 1970-01-01
                      • 1970-01-01
                      • 2015-07-16
                      • 2022-01-26
                      • 2019-09-23
                      相关资源
                      最近更新 更多