为什么要使用内联代码?

时间:2020-03-06 14:42:57  来源:igfitidea点击:

我是C / C ++开发人员,以下几个问题总是让我感到困惑。

  • "常规"代码和内联代码之间有很大区别吗?
  • 主要区别是什么?
  • 内联代码是否只是宏的"形式"?
  • 选择内联代码时必须进行哪种权衡?

谢谢

解决方案

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

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

总而言之,内联是加速多次调用但不在太多地方调用的小型方法的理想选择(100个位置仍然可以,尽管我们需要研究非常极端的示例才能使代码明显膨胀)。

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

如果我们在f.e中将代码标记为内联C ++我们还告诉编译器应该内联执行代码,即。该代码块将"或者多或者少"插入被调用的位置(从而删除堆栈中的推入,弹出和跳转)。因此,可以。如果功能适合这种行为,建议我们这样做。

" inline"就像2000年的" register"等效。不用担心,编译器在决定要优化的内容方面比我们可以做的更好。

这取决于编译器...
假设我们有一个笨拙的编译器。通过指示必须内联的函数,它将在每次调用该函数时将函数内容的副本放入其中。

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

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

是宏吗?并非如此,因为编译器仍会检查参数的类型,等等。

那么智能编译器呢?如果他们"觉得"函数太复杂/太大,他们可以忽略内联指令。也许他们可以自动内联一些琐碎的功能,例如简单的获取器/设置器。

内联标记函数意味着编译器具有<I>选项,可以在调用它的"内联"中包含它(如果编译器选择这样做的话)。相反,宏将始终在原处扩展。内联函数将设置适当的调试符号,以使符号调试器可以在调试宏令人困惑的同时跟踪其来源。内联函数必须是有效函数,而宏...好吧,不是。

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

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

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

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

  • 内联代码是否只是宏的"形式"?

宏和内联代码具有相似之处。最大的不同是,内联代码专门格式化为函数,因此编译器和以后的维护者有更多选择。具体来说,如果我们告诉编译器针对代码空间进行优化,或者将来的维护人员最终对其进行扩展并在代码中的许多地方使用它,则可以轻松地将其转换为函数。

  • 功能:代码空间使用率低,执行速度慢,易于维护
  • 内联功能:代码空间使用率高,执行速度快,易于维护

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

-亚当

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

通过内联,编译器在调用点插入函数的实现。
我们正在执行的操作是消除函数调用开销。
但是,不能保证编译器会内联所有内联的候选对象。但是,对于较小的函数,编译器始终内联。
因此,如果我们有一个被多次调用但仅具有有限数量代码的函数,那么可以从内联中受益,因为函数调用开销可能比函数本身的执行花费更长的时间,因此可以从内联中受益。

简单内联类的吸气剂是一个很好的内联候选人的经典例子。

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'关键字。

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

通常在优化的第3级启用内联(对于GCC,则为-O3)。在某些情况下(可能的话),这可以显着提高速度。

程序中的显式内联可以增加代码大小,从而提高速度。

我们应该看到哪个合适:代码大小或者速度,然后决定是否将其包含在程序中。

我们只需打开优化的第3级,而不必理会它,让编译器完成工作。

Is there a big difference between "regular" code and inline code?

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

Is inline code simply a "form" of macros?

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

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

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

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

What kind of tradeoff must be done when choosing to inline your code?

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

如果我们内联,答案就归结为速度。
如果我们在一个紧密的循环中调用一个函数,而这并不是一个超级巨大的函数,而是在调用该函数时浪费了大量时间,那么将该函数内联,我们将得到很多轰动你的钱。

首先,内联是请求编译器内联函数。因此,由编译器决定是否内联。

  • 什么时候使用?什么时候函数只有很少的几行(适用于所有访问器和mutator),但不适用于递归函数
  • 优点?不涉及调用函数调用所花费的时间
  • 是的,如果在类的头文件中定义了函数,则编译器是否可以内联自己的任何函数?

内联是一种提高速度的技术。但是请使用探查器在情况下对此进行测试。我发现(MSVC)内联并不总是可以实现的,并且肯定不是以任何引人注目的方式进行的。运行时有时会减少百分之几,但在略有不同的情况下会增加百分之几。

如果代码运行缓慢,请退出分析器以查找问题点并进行处理。

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

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

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

表现

正如先前答案中所建议的那样,使用inline关键字可以通过内联函数调用来使代码更快,这通常以增加可执行文件为代价。内联函数调用仅表示在相应地填入参数之后,用函数的实际代码替换对目标函数的调用。

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

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

另外,如果编译器适合,编译器可以并且将忽略" inline"请求。如果无法内联调用函数(即使用非平凡的递归或者函数指针),但是如果函数太大而无法获得有意义的性能,则编译器将执行此操作。

一个定义规则

但是,使用inline关键字声明内联函数还有其他效果,并且可能实际上是满足一个定义规则(ODR)所必需的:C ++标准中的该规则规定,给定符号可以多次声明,但只能被定义一次。如果链接编辑器(=链接器)遇到几个相同的符号定义,它将生成错误。

解决这个问题的一种方法是通过声明它为" static"来赋予编译器内部链接,从而确保编译器不会导出给定的符号。

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

例如,请考虑以下程序:

// 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的函数定义。尽管使用包括防止双重包容的include保护文件来保存文件,但这将导致相同功能的两个定义,尽管使用的是不同的编译单元。

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

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

我们会收到一条错误消息,说重复的符号__Z4meanRKNSt3__16vectorIdNS_9allocatorIdEEEE(这是我们函数" mean"的错误名称)。

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

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