我为什么要使用内联代码?

时间:2008-09-25 11:32:51

标签: c++ optimization inline-functions tradeoff

我是一名C / C ++开发人员,这里有几个让我感到困惑的问题。

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

由于

16 个答案:

答案 0 :(得分:41)

效果

正如之前的答案中所建议的那样,使用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”(这是我们函数mean的{​​{3}})。

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

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

答案 1 :(得分:37)

  
      
  • “常规”代码和内联代码之间有很大区别吗?
  •   

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

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

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

#define unsafe(i) ( (i) >= 0 ? (i) : -(i) )

[...]
unsafe(x++); // x is incremented twice!
unsafe(f()); // f() is called twice!
[...]

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

  
      
  • 选择内联代码时必须做出哪些权衡?
  •   

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

答案 2 :(得分:16)

内联代码本质上就像宏一样,但它是真正的实际代码,可以进行优化。非常小的函数通常适用于内联,因为与该方法所做的少量实际工作相比,设置函数调用所需的工作(将参数加载到适当的寄存器中)是昂贵的。使用内联,不需要设置函数调用,因为代码直接“粘贴”到任何使用它的方法中。

内联增加代码大小,这是它的主要缺点。如果代码太大而无法容纳到CPU缓存中,则可能会出现严重的减速。在极少数情况下,您只需要担心这种情况,因为在很多地方您不太可能使用方法,增加的代码会导致问题。

总而言之,内联非常适合加速多次调用的小方法,但不是太多的地方(不过100个地方仍然没问题 - 你需要进入极端的例子才能获得任何重要的代码膨胀)。

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

答案 3 :(得分:7)

  • “常规”代码和内联代码之间有很大区别吗?

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

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

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

  • 选择内联代码时必须做出哪些权衡?

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

应该注意的是,保存和跳转到函数的寄存器会占用代码空间,因此对于非常小的函数,内联可以占用的空间少于函数。

- 亚当

答案 4 :(得分:2)

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

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

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

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

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

答案 5 :(得分:2)

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

答案 6 :(得分:1)

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

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

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

答案 7 :(得分:0)

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

答案 8 :(得分:0)

“inline”就像2000年代的“注册”。不要打扰,编译器可以更好地决定优化什么。

答案 9 :(得分:0)

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

一个很好的内联候选者的典型例子是简单具体类的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”关键字。

答案 10 :(得分:0)

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

答案 11 :(得分:0)

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

程序中的显式内联可以通过增加代码大小的成本来提高速度。

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

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

答案 12 :(得分:0)

你内心的答案应该归结为速度。 如果你在一个紧密的循环中调用一个函数,并且它不是一个超级巨大的函数,而是在CALLING函数中浪费了很多时间,然后使该函数内联并且你会得到很多爆炸你的降压。

答案 13 :(得分:0)

首先,内联是一个请求编译器内联函数。所以编译器可以内联或不内联。

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

答案 14 :(得分:0)

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

如果代码运行缓慢,请让您的分析器找到麻烦点并进行处理。

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

答案 15 :(得分:0)

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

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