31

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

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

谢谢

4

16 回答 16

43

表现

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

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

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

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

一种定义规则

inline但是,使用关键字声明内联函数有其他效果,实际上可能需要满足单一定义规则 (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

你会收到一条错误消息“duplicate symbol __Z4meanRKNSt3__16vectorIdNS_9allocatorIdEEEE”(这是我们函数的错误名称mean)。

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

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

于 2008-09-25T13:43:43.763 回答
41
  • “常规”代码和内联代码之间有很大区别吗?

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

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

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

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

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

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

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

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

于 2008-09-25T12:10:46.107 回答
16

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

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

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

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

于 2008-09-25T11:36:29.373 回答
7
  • “常规”代码和内联代码之间有很大区别吗?

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

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

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

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

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

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

-亚当

于 2008-09-25T11:40:08.677 回答
2

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

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

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

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

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

于 2008-09-25T11:38:35.887 回答
2

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

于 2008-09-25T11:41:18.767 回答
1

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

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

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

于 2008-09-25T11:40:04.667 回答
0

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

于 2008-09-25T11:37:48.667 回答
0

“内联”就像 2000 年的“注册”一样。不要打扰,编译器可以比您更好地决定要优化的内容。

于 2008-09-25T11:38:31.770 回答
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' 关键字。

于 2008-09-25T11:43:13.367 回答
0

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

于 2008-09-25T11:56:40.400 回答
0

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

程序中的显式内联可以以增加代码大小为代价来提高速度。

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

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

于 2008-09-25T11:57:56.857 回答
0

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

于 2008-09-25T12:13:29.563 回答
0

首先,内联是对编译器内联函数的请求。因此,编译器是否使其内联取决于编译器。

  1. 何时使用?当一个函数的行数很少(对于所有访问器和修改器)但不是递归函数时
  2. 优点?不涉及调用函数调用所花费的时间
  3. 编译器是否内联了它自己的任何函数?是的,当一个函数在类的头文件中定义时
于 2008-09-25T12:28:00.950 回答
0

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

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

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

于 2008-09-25T12:56:46.357 回答
0

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

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

于 2008-09-25T13:20:39.263 回答