什么时候应该为函数/方法编写关键字“inline”?

什么时候应该为C++中的函数/方法编写关键字inline

在看到一些答案后,一些相关的问题:

  • 什么时候我应该在C++中为函数/方法编写关键字“inline”?

  • 什么时候编译器不知道什么时候使函数/方法“内联”?

  • 当一个函数/方法写“内联”时,应用程序是否是多线程的有关系吗?

192490 次浏览

1)如今,几乎从来没有。如果内联一个函数是个好主意,编译器会在没有你帮助的情况下完成它。

2)总是。见#1。

(编辑以反映您将问题分成两个问题…)

实际上,几乎从来没有。你所做的只是建议编译器内联给定的函数(例如,将对该函数的所有调用替换 /w其主体)。当然,不能保证:编译器可能会忽略该指令。

编译器通常会很好地检测+优化这样的事情。

你想把它放在一开始,在返回类型之前。但是大多数编译器都会忽略它。如果它被定义了,并且它的代码块更小,大多数编译器无论如何都会认为它是内联的。

开发和调试代码时,不要使用inline。这会使调试复杂化。

添加它们的主要原因是帮助优化生成的代码。通常这会增加代码空间以换取速度,但有时inline会节省代码空间和执行时间。

在算法完成之前扩展这种关于性能优化的想法是过早优化

在进行模板特化时,您仍然需要显式内联您的函数(如果特化在. h文件中)

当一个人应该内联时:

1.为了避免调用函数时发生的事情的开销,如参数传递、控制转移、控制返回等。

2.函数应该很小,经常被调用,内联是非常有利的,因为按照80-20规则,尽量使那些对程序性能有重大影响的函数内联。

正如我们所知,内联只是一个类似于寄存器的编译器请求,它将花费您的对象代码大小。

哦,伙计,我最讨厌的人之一。

inline更像staticextern,而不是告诉编译器内联你的函数的指令。externstaticinline是链接指令,几乎完全由链接器使用,而不是编译器。

据说inline会向编译器暗示你认为这个函数应该内联。这在1998年可能是真的,但是十年后编译器不需要这样的提示了。更不用说人类在优化代码时通常是错误的,所以大多数编译器都直接忽略了这个“提示”。

  • static-变量/函数名称不能在其他翻译单元中使用。链接器需要确保它不会意外使用来自另一个翻译单元的静态定义的变量/函数。

  • extern-在此翻译单元中使用此变量/函数名称,但如果未定义,请不要抱怨。链接器将对其进行排序并确保所有尝试使用某些extern符号的代码都有其地址。

  • inline-此函数将定义在多个翻译单元中,不用担心。链接器需要确保所有翻译单元使用变量/函数的单个实例。

备注:通常,声明模板inline是没有意义的,因为它们已经具有inline的链接语义学。但是,要使用模板是否需要inline的显式特化和实例化。


具体回答您的问题:

  • <区块链>

    什么时候应该为C++中的函数/方法编写关键字“inline”?

    仅当您希望在头文件中定义函数时。更确切地说,只有当函数的定义可以显示在多个翻译单元中时。在头文件中定义小(如在一个衬里中)函数是个好主意,因为它为编译器提供了更多信息,同时优化您的代码。它还会增加编译时间。

  • <区块链>

    什么时候不应该在C++中为函数/方法编写关键字“inline”?

    不要仅仅因为你认为如果编译器内联它,你的代码会运行得更快就添加内联。

  • <区块链>

    什么时候编译器不知道什么时候使函数/方法“内联”?

    一般来说,编译器在这方面做得比你好。然而,如果编译器没有函数定义,它就无法选择内联代码。在最大化优化的代码中,无论你是否要求,通常所有private方法都被内联。

    为了防止在GCC中内联,请使用__attribute__(( noinline )),在Visual Studio中使用__declspec(noinline)

  • <区块链>

    当为函数/方法编写“内联”时,应用程序是否是多线程是否重要?

    多线程不会以任何方式影响内联。

默认情况下,gcc在编译时不内联任何函数 优化启用。我不知道视觉工作室-deft_code

我通过使用 /FAcs编译并查看汇编代码来检查Visual Studio 9(15.00.30729.01): 编译器在调试模式下未启用优化的情况下生成对成员函数的调用。即使函数标记为__forceinline,也不会生成内联运行时代码。

  • 什么时候编译器不知道什么时候使函数/方法“内联”?

这取决于使用的编译器。不要盲目相信现在的编译器比人类更了解如何内联,而且出于性能原因你永远不应该使用它,因为它是链接指令而不是优化提示。虽然我同意这些论点在意识形态上是正确的,但遇到现实可能是另一回事。

在阅读了周围的多个线程之后,我出于好奇尝试了内联对我正在工作的代码的影响,结果是我为GCC获得了可衡量的加速,而英特尔编译器没有加速。

(更详细:在类外定义的几个关键函数的数学模拟,GCC 4.6.3(g++-O3),ICC 13.1.0(icpc-O3);向临界点添加内联导致GCC代码加速+6%)。

因此,如果您将GCC 4.6定义为现代编译器,那么结果是,如果您编写CPU密集型任务并确切知道瓶颈在哪里,内联指令仍然很重要。

什么时候不应该在C++中为函数/方法编写关键字“inline”?

如果函数在头文件中声明并在.cpp文件中定义,您应该没有写入关键字。

什么时候编译器不知道什么时候使函数/方法“内联”?

没有这种情况。编译器无法使函数内联。它所能做的就是内联对该函数的部分或所有调用。如果它没有函数的代码,它就不能这样做(在这种情况下,链接器需要在能够这样做的情况下这样做)。

当为函数/方法编写“内联”时,应用程序是否是多线程是否重要?

不,那根本不重要。

除非您正在编写库或有特殊原因,否则您可以忘记inline并使用链路时间优化。它消除了函数定义必须在标头中才能考虑跨编译单元内联的要求,这正是inline允许的。

(见有没有理由不使用链接时间优化?

我想用一个令人信服的例子来为这篇文章中所有伟大的答案做出贡献,以驱散任何剩余的误解。

给定两个源文件,例如:

  • inline111.cpp:

    #include <iostream>
    
    
    void bar();
    
    
    inline int fun() {
    return 111;
    }
    
    
    int main() {
    std::cout << "inline111: fun() = " << fun() << ", &fun = " << (void*) &fun;
    bar();
    }
    
  • inline222.cpp:

    #include <iostream>
    
    
    inline int fun() {
    return 222;
    }
    
    
    void bar() {
    std::cout << "inline222: fun() = " << fun() << ", &fun = " << (void*) &fun;
    }
    

  • Case A:

    Compile:

    g++ -std=c++11 inline111.cpp inline222.cpp
    

    产出

    inline111: fun() = 111, &fun = 0x4029a0
    inline222: fun() = 111, &fun = 0x4029a0
    

    讨论

    1. 即使是你,你也应该对内联有相同的定义 函数,C++如果不是这种情况,编译器不会标记它(实际上,由于单独汇编,它无法检查它)。确保这一点是你自己的责任!

    2. 链接器不抱怨一个定义规则,因为fun()被声明为inline。然而,因为inline111.cpp是编译器处理的第一个翻译单元(实际上调用fun()),编译器在inline111.cpp中的第一个调用遇到时实例化fun()。如果编译器在从程序中的其他任何地方(例如frominline222.cpp)调用时决定展开fun(),对fun()的调用将始终链接到从inline111.cpp产生的实例(在inline222.cpp中对fun()的调用也可能在该翻译单元中产生一个实例,但它将保持未链接)。事实上,这从相同的&fun = 0x4029a0打印输出中很明显。

    3. 最后,尽管inline建议编译器实际扩展单行fun(),但它完全忽略了您的建议,这很明显,因为两行都有fun() = 111


  • 案例B:

    编译(通知顺序相反)

    g++ -std=c++11 inline222.cpp inline111.cpp
    

    产出

    inline111: fun() = 222, &fun = 0x402980
    inline222: fun() = 222, &fun = 0x402980
    

    讨论

    1. 这个case断言了在case A中讨论过的内容。

    2. 请注意一个重要的点,如果你注释掉实际调用fun()inline222.cpp例如注释掉cout-语句在inline222.cpp完全),那么,尽管你的翻译单元的编译顺序,fun()将在它的第一次调用遇到在inline111.cpp时被实例化,导致打印输出为inline111: fun() = 111, &fun = 0x402980


  • 案例C:

    编译(通知-O2)

    g++ -std=c++11 -O2 inline222.cpp inline111.cpp
    

    g++ -std=c++11 -O2 inline111.cpp inline222.cpp
    

    产出

    inline111: fun() = 111, &fun = 0x402900
    inline222: fun() = 222, &fun = 0x402900
    

    讨论

    1. 这里描述一样,-O2优化鼓励编译器将可以内联的函数实际上扩大(另请注意,-fno-inline默认,没有优化选项)。从这里的输出可以明显看出,fun()实际上是内联扩展(根据它在-fno-inline0翻译单元中的定义),导致两个-fno-inline1fun()打印输出。尽管如此,仍然有-fno-inline2全局链接的fun()实例(根据标准要求),从-fno-inline3&fun打印输出中可以明显看出。

C++内联与C内联完全不同。

#include <iostream>
extern inline int i[];
int i [5];
struct c {
int function (){return 1;} // implicitly inline
static inline int j = 3; // explicitly inline
static int k; // without inline, a static member has to be defined out of line
static int f (){return 1;} // but a static method does not // implicitly inline
};


extern inline int b;
int b=3;
int c::k = 3; // when a static member is defined out of line it cannot have a static
// specifier and if it doesn't have an `inline` specifier in the
// declaration or on the definition then it is not inline and always
// emits a strong global symbol in the translation unit


int main() {
c j;
std::cout << i;
}

inline本身会影响编译器、汇编器和链接器。这是对编译器的指令,说明只有在翻译单元中使用时才为此函数/数据发出符号,如果是,那么像类方法一样,告诉汇编器将它们存储在单元化数据的.section .text.c::function(),"axG",@progbits,c::function(),comdat.section .bss.i,"awG",@nobits,i,comdat部分中,或初始化数据的.section .data.b,"awG",@progbits,b,comdat部分中。模板实例化也放在它们自己的comdat组中。

这遵循.section name, "flags"MG, @type, entsize, GroupName[, linkage]。例如,部分名称为.text.c::function()axG表示该节是可分配的,可执行的,并且在一个组中,即将指定组名(并且没有M标志,因此不会指定entsize);@progbits表示该节包含数据并且不是空白;c::function()是组名,并且组具有comdat链接,这意味着在所有目标文件中,遇到该组名标记为comdat的所有节都将从最终可执行文件中删除,除了1,即编译器确保翻译单元中只有一个定义,然后告诉汇编器将其放在目标文件中自己的组中(1组中的1节),然后链接器将确保如果inline和不使用inline之间的区别现在对汇编器可见,因此链接器可见,因为由于它们的指令,它不存储在汇编器的常规.data.text等中。只有具有外部链接的内联符号才会像这样给出外部comdat链接——静态链接(本地)符号不需要进入comdat组。

在类中的非静态方法声明上使用inline会在方法定义出线时使方法内联,这将防止该方法在翻译单元中未被引用时在翻译单元中发出。将inline放在出线定义上也可以达到相同的效果。当方法定义出线而没有inline说明符并且类中的声明不是inline时,它将始终在翻译单元中为该方法发出一个符号,因为它将具有外部链接而不是外部comdat链接。如果该方法在类中是定义,则它隐式地是inline,这为它提供了外部comdat链接而不是外部链接。

static inline在类中的成员上(与方法相反)使其成为static成员(不引用其链接-它具有其类的链接,可能是extern)。static inline还允许类的static成员在类内定义,而不需要在类中声明然后定义出线(定义中没有static,如果没有-fpermissive则不允许)。*static inline*还使成员inline而不是static2-inline表示仅在翻译单元中引用时才发出定义。以前,您必须在出线定义上指定inline才能使成员inline

鉴于static方法可以在类中定义,static inline对类中定义的static方法没有影响,该方法始终具有外部链接,是静态方法并且是inline。如果定义超出行,则必须使用inline使其成为inline(即给予外部comdat链接而不仅仅是外部链接),并且仍然不能使用static

文件范围内的static inline仅影响编译器。这对编译器意味着:仅在此函数/数据在翻译单元中使用时才发出符号,并作为常规静态符号(存储in.text /.data不带. Globl指令)。对汇编器来说,现在staticstatic inline没有区别。与inline的其他形式一样,它不能用于class,这是一种类型,但可以用于该类类型的对象。这种形式的static inline也不能用于函数的成员或方法,它将始终被视为inline,因为static表示类中的其他内容(它意味着类充当作用域而不是对象的成员或方法)。

extern inline是一个声明,这意味着你必须在翻译单元中定义这个符号,如果它被引用或抛出编译器错误;如果它被定义了,那么就把它当作一个常规的inline,对于汇编器和链接器来说,extern inlineinline之间没有区别,所以这只是一个编译器保护。

extern inline int i[];
extern int i[]; //allowed repetition of declaration with incomplete type, inherits inline property
extern int i[5]; //declaration now has complete type
extern int i[5]; //allowed redeclaration if it is the same complete type or has not yet been completed
extern int i[6]; //error, redeclaration with different complete type
int i[5]; //definition, must have complete type and same complete type as the declaration if there is a declaration with a complete type

没有错误行的上述全部折叠为inline int i[5]。显然,如果你做了extern inline int i[] = {5};,那么由于通过赋值的显式定义,extern将被忽略。

我认为在没有-fpermissivestatic脱机定义上不允许static的原因是因为它意味着静态引用static链接,因为程序员不能立即明显地看出它是一个类的成员,或者该类是否有,其中static表示不同的东西。在一个简单的整数的情况下,如果c是一个命名空间,k不能从命名空间中定义,但如果k是一个函数,那么就没有办法从代码行中明显地判断它是否是具有static链接的命名空间中函数的越线定义,或者具有外部链接的静态成员的越线定义,并且可能给程序员/代码读者带来错误的印象。

对于本地类,成员/方法上的inline将导致编译器错误,成员和方法没有链接。

有关命名空间上的inline,请参阅这个这个

内联关键字请求编译器用函数的主体替换函数调用,它首先评估表达式,然后通过。它减少了函数调用开销,因为不需要存储返回地址,函数参数也不需要堆栈内存。

何时使用:

  • 为了提高性能
  • 以减少呼叫开销。
  • 由于这只是对编译器的请求,某些函数不会内联 *大型功能
    • 具有太多条件参数的函数
    • 递归代码和带循环的代码等。

F.5:如果一个函数非常小并且时间关键,请内联声明它

原因:一些优化器擅长在没有程序员提示的情况下进行内联,但不要依赖它。测量!在过去的40年左右的时间里,我们被承诺编译器在没有人类提示的情况下可以比人类更好地内联。我们仍在等待。指定内联(显式地,或者在类定义中编写成员函数时隐式地)会鼓励编译器做得更好。

图片来源:https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#Rf-inline

有关示例和异常,请转到源(见上文)。

继承可能会出现一个用例。例如,如果以下所有情况都为真:

  • 你有一个基类
  • 基类需要抽象
  • 基类除了析构函数之外没有纯虚方法
  • 您不想为基类创建cpp文件,因为这是徒劳的

然后你必须定义析构函数;否则,你会有一些undefined referance链接器错误。此外,你不仅要定义,还要使用内联关键字定义析构函数;否则,你会有multiple definition链接器错误。

对于一些只包含静态方法或编写基异常类等的辅助类,可能会发生这种情况。

让我们举个例子:

Base. h:

class Base {
public:
Base(SomeElementType someElement) noexcept : _someElement(std::move(someElement)) {}


virtual ~Base() = 0;


protected:
SomeElementType _someElement;
}


inline Base::~Base() = default;

派生1. h:

#include "Base.h"


class Derived1 : public Base {
public:
Derived1(SomeElementType someElement) noexcept : Base(std::move(someElement)) {}


void DoSomething1() const;
}

Derived1.cpp:

#include "Derived1.h"


void Derived1::DoSomething1() const {
// use _someElement
}

派生2. h:

#include "Base.h"


class Derived2 : public Base {
public:
Derived2(SomeElementType someElement) noexcept : Base(std::move(someElement)) {}


void DoSomething2() const;
}

Derived2.cpp:

#include "Derived2.h"


void Derived2::DoSomething2() const {
// use _someElement
}

一般来说,抽象类除了构造函数和析构函数之外,还有一些纯虚方法。所以,你不必分离基类的解密和虚拟析构函数的定义,你可以在类解密上写virtual ~Base() = default;。然而,在我们的例子中,它不是那样的。

据我所知,MSVC允许你在类解密上编写类似的东西:virtual ~Base() = 0 {}。所以你不需要用内联关键字分离解密和定义。但它只适用于MSVC编译器。

真实世界的例子:

基本异常. h:

#pragma once


#include <string>


class BaseException : public std::exception {
public:
BaseException(std::string message) noexcept : message(std::move(message)) {}
virtual char const* what() const noexcept { return message.c_str(); }


virtual ~BaseException() = 0;


private:
std::string message;
};


inline BaseException::~BaseException() = default;

例外. h:

#pragma once


#include "BaseException.h"


class SomeException : public BaseException {
public:
SomeException(std::string message) noexcept : BaseException(std::move(message)) {}
};

其他例外. h:

#pragma once


#include "BaseException.h"


class SomeOtherException : public BaseException {
public:
SomeOtherException(std::string message) noexcept : BaseException(std::move(message)) {}
};

main.cpp:

#include <SomeException.h>
#include <SomeOtherException.h>


#include <iostream>


using namespace std;


static int DoSomething(int argc) {
try {
switch (argc) {
case 0:
throw SomeException("some");
case 1:
throw SomeOtherException("some other");
default:
return 0;
}
}
catch (const exception& ex) {
cout << ex.what() << endl;
return 1;
}
}


int main(int argc, char**) {
return DoSomething(argc);
}