内联函数
inline
函数有什么特别之处?
当编译器内联展开函数调用时,函数的代码会插入到调用者的代码流中(概念上类似于 #define
宏)。这可以,取决于一万亿其他因素,提高性能,因为优化器可以对被调用的代码进行过程集成——将内联的代码优化到调用者中。
有几种方法可以指定一个函数是 inline
,其中一些涉及 inline
关键字,另一些则不涉及。无论您如何将函数指定为 inline
,这都是一个编译器可以忽略的请求:编译器可能会内联展开您调用指定为 inline
的函数的一些、全部或不展开任何地方。(如果这看起来模糊不清,请不要气馁。上述灵活性实际上是一个巨大的优势:它允许编译器以不同于小函数的方式处理大函数,并且如果您选择正确的编译器选项,它允许编译器生成易于调试的代码。)
过程集成的一个简单例子是什么?
考虑以下对函数 g()
的调用
void f()
{
int x = /*...*/;
int y = /*...*/;
int z = /*...*/;
// ...code that uses x, y and z...
g(x, y, z);
// ...more code that uses x, y and z...
}
假设一个典型的 C++ 实现有寄存器和堆栈,在调用 g()
之前,寄存器和参数会被写入堆栈,然后参数在 g()
内部从堆栈中读取,并在 g()
返回到 f()
时再次读取以恢复寄存器。但这会产生很多不必要的读写操作,尤其是在编译器能够将变量 x
、y
和 z
用于寄存器的情况下:每个变量可能被写入两次(作为寄存器和参数)并读取两次(在 g()
中使用时以及在返回 f()
期间恢复寄存器时)。
void g(int x, int y, int z)
{
// ...code that uses x, y and z...
}
如果编译器内联展开对 g()
的调用,所有这些内存操作都可能消失。寄存器不需要被写入或读取,因为没有函数调用,并且参数不需要被写入或读取,因为优化器知道它们已经在寄存器中。
当然,您的实际情况可能有所不同,并且有大量超出本 FAQ 范围的变量,但上述内容可以作为过程集成可能发生的事情的示例。
inline
函数能提高性能吗?
是也不是。有时。也许吧。
没有简单的答案。inline
函数可能会使代码更快,也可能使其更慢。它们可能会使可执行文件更大,也可能使其更小。它们可能会导致抖动,也可能阻止抖动。而且它们可能,而且通常,与速度完全无关。
inline
函数可能会使其更快:如上所述,过程集成可能会消除许多不必要的指令,这可能会使程序运行得更快。
inline
函数可能会使其更慢:过多的内联可能导致代码膨胀,这可能导致按需分页虚拟内存系统上的“抖动”。换句话说,如果可执行文件太大,系统可能会花费大部分时间从磁盘获取下一块代码。
inline
函数可能会使其更大:这就是上面描述的代码膨胀的概念。例如,如果一个系统有 100 个 inline
函数,每个函数展开为 100 字节的可执行代码,并且在 100 个地方被调用,那么这将增加 1MB。这 1MB 会导致问题吗?谁知道呢,但最后这 1MB 有可能导致系统“抖动”,从而减慢速度。
inline
函数可能会使其更小:编译器通常会生成比内联展开函数体更多的代码来推送/弹出寄存器/参数。这发生在非常小的函数中,也发生在大型函数中,当优化器能够通过过程集成消除大量冗余代码时——也就是说,当优化器能够使大型函数变小时。
inline
函数可能会导致抖动:内联可能会增加二进制可执行文件的大小,这可能会导致抖动。
inline
函数可能会阻止抖动:即使可执行文件大小增加,工作集大小(需要同时存在于内存中的页数)也可能下降。当 f()
调用 g()
时,代码通常位于两个不同的页面上;当编译器将 g()
的代码过程集成到 f()
中时,代码通常位于同一页面上。
inline
函数可能会增加缓存未命中次数:内联可能导致内部循环跨越内存缓存的多行,这可能导致内存缓存的抖动。
inline
函数可能会减少缓存未命中次数:内联通常会提高二进制代码中的引用局部性,这可能会减少存储内部循环代码所需的缓存行数。这最终可能使 CPU 密集型应用程序运行得更快。
inline
函数可能与速度无关:大多数系统都不是 CPU 密集型的。大多数系统是 I/O 密集型、数据库密集型或网络密集型,这意味着系统整体性能的瓶颈是文件系统、数据库或网络。除非您的“CPU 表”始终处于 100%,否则 inline
函数可能不会使您的系统更快。(即使在 CPU 密集型系统中,inline
也只有在瓶颈本身中使用时才会有帮助,而瓶颈通常只存在于一小部分代码中。)
没有简单的答案:您必须亲身尝试才能看出什么效果最好。不要满足于简单的答案,例如“永远不要使用 inline
函数”或“总是使用 inline
函数”或“当且仅当函数小于 N 行代码时才使用 inline
函数”。这些一刀切的规则可能很容易写下来,但它们会产生次优结果。
inline
函数如何帮助平衡安全性与速度?
在纯 C 语言中,可以通过在 struct
中放置一个 void*
来实现“封装的 struct
”,其中 void*
指向对 struct
用户未知的实际数据。因此,struct
的用户不知道如何解释 void*
指向的内容,但访问函数将 void*
转换为适当的隐藏类型。这提供了一种封装形式。
不幸的是,它放弃了类型安全,并且即使访问 struct
的微不足道的字段也需要进行函数调用(如果您允许直接访问 struct
的字段,那么任何人都可以获得直接访问,因为他们必然会知道如何解释 void*
指向的内容;这将使更改底层数据结构变得困难)。
函数调用开销很小,但可以累积。C++ 类允许函数调用被 inline
展开。这让您可以同时拥有封装的安全性以及直接访问的速度。此外,这些 inline
函数的参数类型由编译器检查,这比 C 的 #define
宏有所改进。
为什么我应该使用 inline
函数而不是普通的 #define
宏?
因为 #define
宏以 4 种方式“邪恶”:邪恶 #1,邪恶 #2,邪恶 #3,以及邪恶 #4。有时您仍然应该使用它们,但它们仍然是邪恶的。
与 #define
宏不同,inline
函数避免了臭名昭著的宏错误,因为 inline
函数总是精确地评估每个参数一次。换句话说,调用 inline
函数在语义上就像调用常规函数一样,只是更快。
// A macro that returns the absolute value of i
#define unsafe(i) \
( (i) >= 0 ? (i) : -(i) )
// An inline function that returns the absolute value of i
inline
int safe(int i)
{
return i >= 0 ? i : -i;
}
int f();
void userCode(int x)
{
int ans;
ans = unsafe(x++); // Error! x is incremented twice
ans = unsafe(f()); // Danger! f() is called twice
ans = safe(x++); // Correct! x is incremented once
ans = safe(f()); // Correct! f() is called once
}
此外,与宏不同,参数类型会被检查,并且会正确执行必要的转换。
宏有害健康;除非万不得已,否则不要使用它们。
如何告诉编译器将非成员函数 inline
?
当你声明一个 inline
函数时,它看起来就像一个普通函数
void f(int i, char c);
但是当你定义一个 inline
函数时,你会在函数定义前加上关键字 inline
,并将定义放在头文件中
inline
void f(int i, char c)
{
// ...
}
注意:函数的定义({...}
之间的部分)必须放在头文件中,除非该函数仅在单个 .cpp 文件中使用。特别是,如果你将 inline
函数的定义放在 .cpp
文件中,并且从其他 .cpp
文件中调用它,你将收到链接器的“未解析的外部符号”错误。
如何告诉编译器将成员函数 inline
?
inline
成员函数的声明看起来就像非 inline
成员函数的声明
class Fred {
public:
void f(int i, char c);
};
但是当你定义一个 inline
成员函数({...}
部分)时,你会在成员函数定义前加上关键字 inline
,并且你(几乎总是)将定义放在头文件中
inline
void Fred::f(int i, char c)
{
// ...
}
你(几乎总是)将 inline
函数的定义({...}
部分)放在头文件中的原因是避免链接器报告“未解析的外部符号”错误。如果你将 inline
函数的定义放在 .cpp
文件中,并且该函数从其他 .cpp
文件中调用,就会出现这个错误。
还有其他方法可以告诉编译器将成员函数 inline
吗?
是的:在类体内直接定义成员函数
class Fred {
public:
void f(int i, char c)
{
// ...
}
};
这通常比在类体外部定义 inline
函数的替代方案更方便。然而,尽管它对编写类的人来说更容易,但对所有读者来说更难,因为它将类做什么(外部行为)与如何做(实现)混合在一起。由于这种混合,如果您的类旨在高度重用并且您的类文档就是头文件本身,您应该在类体外部定义所有成员函数。这是斯波克逻辑的另一个应用:多数人的需求(所有重用您的类的人)超过少数人(维护您的类实现的人)或一个人(类的原始作者)的需求。
当然,如果您没有编写高度可重用的类,或者您在头文件之外提供类的外部行为文档(例如,HTML 或 PDF 或其他),那么您应该在类体内部定义您的 inline
函数,因为这将简化您的开发以及类的实现的维护。
这种方法在下一个 FAQ 中得到了进一步的利用。
对于在类外部定义的 inline
成员函数,是最好将 inline
关键字放在类体内的声明旁边,类体外的定义旁边,还是两者都放?
仅定义处。
这是一个在类体外部定义的 inline
成员函数的示例
class Foo {
public:
void method(); // Best practice: Don't put the inline keyword here
// ...
};
inline void Foo::method() // Best practice: Put the inline keyword here
{
// ...
}
请记住,当您的类旨在高度重用且您的重用者将阅读您的头文件以确定类做什么(其可观察的语义或外部行为)时,您应该在类体外部定义您的 inline
成员函数。在这种情况下……
- 类体中的
public:
部分是您描述类的可观察语义、其公共成员函数、友元函数以及其他供他人重用的类特性。目标是保持此public:
部分的公开性——清除public:
部分中任何对重用者不重要的东西。如果“它”不能从调用者的代码中观察到,“它”就不应该出现在类体的public:
部分。 - 类的其他部分,包括类体中非
public:
的部分、成员函数和友元函数的定义等,纯粹是实现。尽量不要描述任何尚未在类的public:
部分中描述的可观察语义。如果“它”可以从调用者的代码中观察到,“它”应该在类体的public:
部分中描述;“它”也可能出现在类的非public:
部分中,但“它”应该以某种方式在public:
部分中指定。
从实际角度来看,这种分离使您的类的重用者生活更轻松、更安全。假设查克只想简单地使用您的可重用类。因为您阅读了本 FAQ 并使用了上述分离,查克将在您的类的 public:
部分中看到他需要看到的一切,而不需要看到任何不需要看到的东西。您的类的 public:
部分将是查克了解您的类的可观察语义,即外部行为的一站式商店。通过净化您的类的 public:
部分,您让查克的生活既轻松(他只需要在一个地方查找)又安全(他纯洁的思维不会被实现细节所污染)。
回到内联性:函数是否内联的决定是一个实现细节,它不会改变函数调用的可观察语义(“含义”)。因此,inline
关键字不应出现在类的 public:
(或 protected:
或 private:
)部分中,它需要出现在函数定义的旁边。
*注意:大多数人使用“声明”和“定义”这两个术语来区分上述两个位置。例如,他们可能会说:“我应该将 inline
关键字放在声明旁边还是定义旁边?”如果你正在与一位语言律师交流,更准确的说法是“非定义性声明”和“定义性声明”,因为定义也是声明。