何时应该inline
在 C++ 中为函数/方法编写关键字?
看到一些答案后,一些相关的问题:
什么时候不应该为 C++ 中的函数/方法编写关键字“内联”?
编译器何时不知道何时将函数/方法“内联”?
当为函数/方法编写“内联”时,应用程序是否是多线程的是否重要?
何时应该inline
在 C++ 中为函数/方法编写关键字?
看到一些答案后,一些相关的问题:
什么时候不应该为 C++ 中的函数/方法编写关键字“内联”?
编译器何时不知道何时将函数/方法“内联”?
当为函数/方法编写“内联”时,应用程序是否是多线程的是否重要?
哦,伙计,我最讨厌的事情之一。
inline
更像static
或extern
比一个指令告诉编译器内联你的函数。 extern
, static
,inline
是链接指令,几乎只由链接器使用,而不是编译器。
据说inline
向编译器提示您认为该函数应该被内联。这在 1998 年可能是正确的,但十年后编译器不需要这样的提示。更不用说在优化代码时人类通常是错误的,所以大多数编译器完全忽略了“提示”。
static
- 变量/函数名称不能在其他翻译单元中使用。链接器需要确保它不会意外使用来自另一个翻译单元的静态定义的变量/函数。
extern
- 在此翻译单元中使用此变量/函数名称,但如果未定义,请不要抱怨。链接器将对其进行排序,并确保所有尝试使用外部符号的代码都有其地址。
inline
- 这个函数会在多个翻译单元中定义,不用担心。链接器需要确保所有翻译单元都使用变量/函数的单个实例。
注意:通常,声明模板inline
是没有意义的,因为它们已经具有链接语义inline
。但是,需要inline
使用模板的显式特化和实例化。
您的问题的具体答案:
我什么时候应该为 C++ 中的函数/方法编写关键字“内联”?
仅当您希望在标题中定义函数时。更准确地说,只有当函数的定义可以出现在多个翻译单元中时。在头文件中定义小(如在一个衬里)函数是一个好主意,因为它为编译器提供了更多信息以在优化代码时使用。它还增加了编译时间。
什么时候不应该为 C++ 中的函数/方法编写关键字“内联”?
不要仅仅因为您认为如果编译器内联您的代码会运行得更快,就添加内联。
编译器何时不知道何时将函数/方法“内联”?
一般来说,编译器会比你做得更好。但是,如果没有函数定义,编译器就没有内联代码的选项。在最大限度优化的代码private
中,无论您是否要求,通常所有方法都内联。
为了防止在 GCC 中内联,请使用__attribute__(( noinline ))
,而在 Visual Studio 中,请使用__declspec(noinline)
.
当为函数/方法编写“内联”时,应用程序是否是多线程的是否重要?
多线程不会以任何方式影响内联。
我想通过一个令人信服的例子来为这个线程中的所有重要答案做出贡献,以消除任何剩余的误解。
给定两个源文件,例如:
内联111.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;
}
案例一:
编译:
g++ -std=c++11 inline111.cpp inline222.cpp
输出:
inline111: fun() = 111, &fun = 0x4029a0
inline222: fun() = 111, &fun = 0x4029a0
讨论:
即使您应该对内联函数有相同的定义,如果不是这种情况,C++ 编译器也不会标记它(实际上,由于单独编译,它无法检查它)。确保这一点是您自己的责任!
链接器不会抱怨单一定义规则,正如fun()
声明的那样inline
。但是,因为inline111.cpp是编译器处理的第一个翻译单元(实际上调用fun()
),所以编译器在inline111.cppfun()
中的第一个调用遇到时实例化。如果编译器决定不扩展它从程序中其他任何地方的调用(例如从inline222.cpp),则调用将始终链接到它从inline111.cpp生成的实例(对inline222.cpp内部的调用fun()
fun()
fun()
也可能在该翻译单元中产生一个实例,但它会保持未链接)。事实上,从相同的&fun = 0x4029a0
打印输出中可以明显看出这一点。
最后,尽管inline
建议编译器实际扩展one-liner fun()
,但它完全忽略了您的建议,这很清楚,因为fun() = 111
在这两行中。
案例B:
编译 (注意逆序):
g++ -std=c++11 inline222.cpp inline111.cpp
输出:
inline111: fun() = 222, &fun = 0x402980
inline222: fun() = 222, &fun = 0x402980
讨论:
这个案例断言了案例 A中讨论的内容。
请注意重要的一点,如果您注释掉对 inline222.cpp 的实际调用fun()
(例如完全注释掉inline222.cppcout
中的语句),那么尽管您的翻译单元的编译顺序不同,但在fun()
第一次调用遇到inline111.cpp,导致案例 B的打印输出为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
讨论:
-O2
优化鼓励编译器实际扩展可内联的函数(另请注意,-fno-inline
默认情况下没有优化选项)。从这里的输出可以明显看出,fun()
实际上已经内联扩展(根据它在特定翻译单元中的定义),导致两个不同 fun()
的打印输出。尽管如此,仍然只有一个全局链接的实例fun()
(根据标准的要求),从相同 &fun
的打印输出可以看出。在进行模板特化时,您仍然需要显式内联您的函数(如果特化在 .h 文件中)
1)如今,几乎从来没有。如果内联函数是个好主意,编译器会在没有你帮助的情况下完成它。
2) 总是。见#1。
(编辑以反映您将问题分为两个问题......)
什么时候不应该为 C++ 中的函数/方法编写关键字“内联”?
如果函数在头文件中声明并在.cpp
文件中定义,则不应编写关键字。
编译器何时不知道何时将函数/方法“内联”?
没有这种情况。编译器不能使函数内联。它所能做的就是内联对函数的部分或全部调用。如果它没有函数的代码,它就不能这样做(在这种情况下,如果它能够这样做,链接器需要这样做)。
当为函数/方法编写“内联”时,应用程序是否是多线程的是否重要?
不,这根本不重要。
这取决于使用的编译器。不要盲目相信现在的编译器比人类更了解如何内联,并且出于性能原因你永远不应该使用它,因为它是链接指令而不是优化提示。虽然我同意这些论点在意识形态上是否正确,但遇到现实可能是另一回事。
在阅读了多个线程之后,我出于好奇尝试了内联对我正在工作的代码的影响,结果是我对 GCC 获得了可测量的加速,而对英特尔编译器没有加速。
(更多细节:在类之外定义了少量关键函数的数学模拟,GCC 4.6.3 (g++ -O3),ICC 13.1.0 (icpc -O3);在关键点添加内联导致 GCC 代码加速 + 6%)。
因此,如果您将 GCC 4.6 限定为现代编译器,那么如果您编写 CPU 密集型任务并且知道瓶颈到底在哪里,那么 inline 指令仍然很重要。
实际上,几乎从来没有。您所做的只是建议编译器将给定的函数内联(例如,替换对该函数的所有调用/w 它的主体)。当然,不能保证:编译器可能会忽略该指令。
编译器通常会很好地检测+优化这样的事情。
默认情况下,gcc 在未启用优化的情况下编译时不会内联任何函数。我不了解 Visual Studio – deft_code
我通过使用 /FAcs 编译并查看汇编代码检查了 Visual Studio 9 (15.00.30729.01) 的情况:编译器生成了对成员函数的调用,而在调试模式下未启用优化。即使函数用__forceinline标记,也不会生成内联运行时代码。
F.5:如果函数非常小且时间紧迫,则将其声明为内联
原因:一些优化器擅长在没有程序员提示的情况下进行内联,但不要依赖它。措施!在过去 40 年左右的时间里,我们得到承诺,编译器可以在没有人类提示的情况下比人类更好地内联。我们还在等。指定内联(在类定义中编写成员函数时显式或隐式)鼓励编译器做得更好。
来源:https ://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#Rf-inline
有关示例和例外情况,请转到来源(见上文)。
一个用例可能发生在继承上。例如,如果以下所有情况都为真:
然后你必须定义析构函数;否则,您将遇到一些undefined referance
链接器错误。此外,您不仅要定义,还要使用 inline 关键字定义析构函数;否则,您将遇到multiple definition
链接器错误。
这可能发生在一些只包含静态方法或编写基础异常类等的辅助类上。
我们举个例子:
基数.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;
}
派生1.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;
}
派生2.cpp:
#include "Derived2.h"
void Derived2::DoSomething2() const {
// use _someElement
}
通常,抽象类除了构造函数或析构函数之外还有一些纯虚方法。因此,您不必将基类的虚拟析构函数的声明和定义分开,您只需编写virtual ~Base() = default;
类声明即可。但是,在我们的案例中,情况并非如此。
据我所知,MSVC 允许您在 class decleration 上编写类似的内容:virtual ~Base() = 0 {}
. 因此,您不需要使用 inline 关键字将 decleration 和定义分开。但它只适用于 MSVC 编译器。
现实世界的例子:
BaseException.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;
SomeException.h:
#pragma once
#include "BaseException.h"
class SomeException : public BaseException {
public:
SomeException(std::string message) noexcept : BaseException(std::move(message)) {}
};
SomeOtherException.h:
#pragma once
#include "BaseException.h"
class SomeOtherException : public BaseException {
public:
SomeOtherException(std::string message) noexcept : BaseException(std::move(message)) {}
};
主.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);
}
C++ inline 与C inline完全不同。
#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 节),然后链接器将确保如果任何目标文件具有同名的组,则仅在最终的 .exe 中包含一个。汇编器和链接器现在可以看到inline
和不使用之间的区别,因为它没有存储在常规或inline
.data
.text
由于它们的指令,汇编器等。只有具有外部链接的内联符号才会被赋予这样的外部 comdat 链接——静态链接(本地)符号不需要进入 comdat 组。
inline
在类中的非静态方法声明上,如果方法被离线定义,则使方法内联,如果在翻译单元中未引用该方法,这将防止在翻译单元中发出该方法。通过使用外联定义可以达到相同的效果inline
。当一个方法在没有inline
说明符的情况下被离线定义并且类中的声明不是inline
,那么它将始终在翻译单元中为该方法发出一个符号,因为它将具有外部链接而不是外部 comdat 链接。如果方法是在类中定义的,那么它是隐式的inline
,这给了它外部 comdat 链接而不是外部链接。
static inline
在类中的成员上(与方法相反)使其成为static
成员(不指代它的链接——它具有其类的链接,可能是外部的)。static inline
还允许static
在类中定义类的成员,而不是需要在类中声明,然后在行外定义(没有static
在定义中,没有 是不允许的-fpermissive
)。*static inline*
也使成员inline
而不是static inline
-inline
意味着仅在翻译单元中引用定义时才发出定义。以前,您必须inline
在行外定义上指定才能使成员inline
.
看到static
方法可以在类中定义,对类中定义static inline
的方法没有影响static
,总是有外部链接,是静态方法,是inline
. 如果它被定义了,那么inline
必须使用它来制作它inline
(即给予外部comdat链接,而不仅仅是外部链接),static
仍然不能使用。
static inline
在文件范围内只影响编译器。这对编译器意味着:只有在翻译单元中使用此函数/数据时才发出一个符号,并将其作为常规静态符号(存储在 .text /.data 中,而不使用 .globl 指令)。static
对于汇编程序来说,现在和之间没有区别static inline
。与 的其他形式一样inline
,它不能用于class
类型,但可以用于该类类型的对象。这种形式的static inline
也不能用于函数的成员或方法,它总是被视为类inline
中的static
其他含义(这意味着该类充当范围而不是它的成员或方法用于对象)。
extern inline
是一个声明,意味着您必须在翻译单元中定义此符号,如果它被引用或抛出编译器错误;如果已定义,则将其视为常规inline
,并且对于汇编器和链接器而言, and 之间将没有区别extern inline
,inline
因此这仅是编译器保护。
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
由于通过赋值的显式定义而被忽略。
我认为在外线定义中static
不允许的原因是因为它暗示静态指的是链接,因为程序员并不能立即看出它是一个类的成员或者该类是否具有 ,这意味着不同的东西。忽略线外定义上的说明符,它没有任何意义。在简单整数的情况下,不能在命名空间之外定义,如果是命名空间,但如果是函数,则无法从代码行明显看出它是否是异常的在命名空间中定义函数static
-fpermissive
static
static
-fpermissive
static
k
c
k
static
链接,或带有外部链接的静态成员的离线定义,可能会给代码的程序员/读者带来错误的印象。
对于本地类,inline
在成员/方法上将导致编译器错误并且成员和方法没有链接。
除非您正在编写库或有特殊原因,否则您可以忘记inline
并使用链接时优化。它消除了函数定义必须在头文件中才能考虑跨编译单元内联的要求,这正是inline
允许的。
(但请参阅有什么理由不使用链接时间优化吗?)
inline关键字要求编译器将函数调用替换为函数体,它首先计算表达式然后传递。它减少了函数调用开销,因为不需要存储返回地址并且函数不需要堆栈内存论据。
何时使用:
- 提高性能
- 减少通话开销。
- 由于这只是对编译器的请求,因此某些函数不会被内联 *large 函数
- 具有太多条件参数的函数
- 递归代码和带有循环的代码等。
在开发和调试代码时,inline
省略。它使调试复杂化。
添加它们的主要原因是帮助优化生成的代码。通常,这会以增加的代码空间换取速度,但有时会inline
同时节省代码空间和执行时间。
在算法完成之前扩展这种关于性能优化的想法是过早的优化。
你想把它放在最开始,在返回类型之前。但是大多数编译器都忽略了它。如果它被定义,并且它有一个更小的代码块,大多数编译器无论如何都会认为它是内联的。
什么时候应该内联:
1.当想要避免调用函数时发生的事情的开销,如参数传递、控制转移、控制返回等。
2.函数要小,经常调用,内联是非常有利的,因为按照80-20规则,尽量将那些对程序性能有重大影响的函数内联。
正如我们所知,内联只是对编译器的请求,类似于寄存器,它会花费你的对象代码大小。