当前位置: 首页 > 文档资料 > C++ FAQ Lite >

[9] 内联函数

优质
小牛编辑
130浏览
2023-12-01

FAQs in section [9]:

9.1 内联函数有什么用?

当编译器内联展开一个函数调用时,该函数的代码会被插入到调用代码流中(概念上类似于展开#define宏)。这能够改善性能(当然还有很多其它因素),因为优化器能够顺序集成(procedurally integrate)被调用代码,即将被调用代码直接优化进调用代码中。

有几种方法将一个函数设定为内联。其中一些需要使用inline关键字),还有一些则不需要。 不管你用何种方法设定函数为内联,这只是个请求,而编译器可以忽略它。编译器可能会展开内联函数调用,也可能不展开。(这看上去非常模糊,但不要为之沮 丧。这种灵活性其实有很大优点:这可以让编译器能够区别对待很长的函数和短的函数,另外如果选择了正确的编译选项,还能使编译器生成易于调试的代码。)

9.2 有没有个简单的例子说明什么是顺序集成(procedure integration)?

考虑下面对函数g()的调用:

 void f()
 {
   int x = /*...*/;
   int y = /*...*/;
   int z = /*...*/;
   ...使用x, y 和 z的代码...
   g(x, y, z);
   ...更多使用x, y 和 z的代码...
 }

假设一个典型的C++实现包含有一系列寄存器和一个栈,在调用g()之前,寄存器和参数会被写入栈中,然后在g()内部的栈中会读出参数值,然后在 g()返回到f()时又会将这些寄存器的值读出来并恢复到寄存器中。但这里面有很多不必要的读写操作,尤其是当编译器能够用寄存器来保存xyz时。每 个变量都会写两次(做为寄存器和做为参数)并且读两次(在g()内部使用和返回到f()时恢复寄存器)。

 void g(int x, int y, int z)
 {
   ...使用x、 y 和 z的代码...
 }

如果编译器能够内联展开对g()的调用,那么所有这些内存操作就都会消失了。不用再读写寄存器了,因为根本没有函数调用。各个参数也不必再被读写了,因为优化器知道它们已经在寄存器里了。

当然你所能获得的好处可能会变化,在本FAQ之外还有很多很多变数。但以上的例子能够揭示出顺序集成时所发生的事情。

9.3 内联函数能改善性能么?

可能会,也可能不会。有时可以。也许可以。

答案没那么简单。内联函数可能会使代码速度更快,也可能使速度变慢。可能会使可执行文件变大,也可能变小。可能会导致系统性能下降,也可能避免性能下降。内联函数可能(经常是)与速度完全无关。

内联函数可能会使代码速度更快正如上面所说,顺序集成可能会移除很多不必要的指令,这可能会加快速度。

内联函数可能会使代码速度更慢过多的内联可能会使代码膨胀,在使用分页虚拟内存的系统上,这可能会导致性能下降。换句话说,如果可执行文件过大,系统可能会花费很多时间到磁盘上获取下一块代码。

内联函数可能会增加可执行文件尺寸:这就是上面所说的代码膨胀。例如,假设系统有100个内联函数,每个展开后有100字节,并且被调用了100次。这就会增加1MB的大小。增加这么1MB会导致问题吗?谁知道呢,但很可能就是这1MB导致系统性能下降。

内联函数可能会减少可执行文件尺寸:如果不内联展开函数体,编译器可能会要产生更多代码来压入/弹出寄存器内容和参数。对于很小的函数来说会是这样。如果优化器能够通过顺序集成消除雕大量冗余代码的话,那么对大函数也会起作用(也就是说,优化器能够使大函数变小)。

内联函数可能会导致系统性能下降内联可能会导致二进制可执行文件尺寸变大,由此导致系统性能下降。

内联函数可能会避免系统性能下降:即使可执行文件尺寸变大,当前正在使用的物理内存数量(即需要同时留在内存中的页面数量)却仍然可能降低。当f()调用g()时,代码经常分散在2个不同的页面上。当编译器将g()的代码顺序集成到f()后,代码通常会放在一个页面上。

内联函数可能会降低缓存的命中率:内联可能会导致内层循环跨越多行的内存缓存,这可能会导致内存和缓存频繁交换,从而性能下降。

内联函数可能会提高缓存的命中率:内联通常能够在二进制代码中就近安排所用到的内容,这可能会减少用来存放内层循环代码的缓存数量。最终这会使CPU密集型程序跑得更快。

内联函数可能与速度无关大多数系统不是CPU密集型的,而使I/O密集型的、数据库密集型的或是网 络密集型的。这表明系统的瓶颈存在于文件系统、数据库或网络。除非你的“CPU速度表”指示是100%,否则内联函数可能不会使你的系统速度更快。(即使 是CPU密集型的系统,也只有在被用到瓶颈之处时,内联才会有帮助。而瓶颈通常只存在于很少一部分代码中。)

没有简单定论:你需要多试验来找到最佳方案。不要指望依赖那些过分简化的答案,比如“绝不要使用内联函数”,或者“总是使用内联函数”,再比如“当且仅当函数体少于N行代码时使用内联函数”。这种以一盖全的准则写下来很容易,但却会产生不够优化的结果。

译注:这一小节中的性能下降主要是指系统因为频繁交换内存页而导致的性能下降。原文是thrashing。

9.4 内联函数如何在安全和速度上取得折衷?

在 C 中,你可以通过在结构中设置一个 void* 来得到“封装的结构”,在这种情况下,指向实际数据的 void* 指针对于结构的用户来说是未知的。因此结构的用户不知道如何解释void*指针所指内容,但是存取函数可以将 void* 转换成适当的隐含类型。这样给出了封装的一种形式。

不幸的是这样做丧失了类型安全,并且即使仅仅是访问结构体中的一个很不重要的字段也必须进行函数调用。(如果你允许直接存取结构的域,那么任何人都能直接存取该结构体了,因为他们必须了解如何解释 void* 指针所指内容;这样将使改变底层数据结构变的困难)。

虽然函数调用开销是很小的,但它会被累积。C++类允许函数调用以内联展开。这样让你在得到封装的安全性时,同时得到直接存取的速度。此外,内联函数的参数类型由编译器检查,这是对 C 的 #define 宏的一个改进。

9.5 为什么我应该用内联函数?而不是原来清晰的 #define 宏?

因为#define宏有四宗罪:罪状#1, 罪状#2, 罪状#3, 和 罪状#4。有时虽然你会用它们,但它们仍然是邪恶的。

和 #define 宏不同的是,内联函数总是对参数只精确地进行一次求值,从而避免了那声名狼藉的宏错误。换句话说,调用内联函数和调用正规函数是等价的,差别仅仅是更快:

 // 返回 i 的绝对值的宏
 #define unsafe(i)  \
         ( (i) >= 0 ? (i) : -(i) )

 // 返回 i 的绝对值的内联函数
 inline
 int safe(int i)
 {
   return i >= 0 ? i : -i;
 }

 int f();

 void userCode(int x)
 {
   int ans;

   ans = unsafe(x++);   // 错误!x 被增加两次
   ans = unsafe(f());   // 危险!f()被调用两次

   ans = safe(x++);     // 正确! x 被增加一次
   ans = safe(f());     // 正确! f() 被调用一次
 }

和宏不同的,还有内联函数的参数类型被检查,并且被正确地进行必要的转换。

宏是有害的;非万不得已不要用。

9.6 如何告诉编译器使非成员函数成为内联函数?

声明内联函数看上去和普通函数非常相似:

void f(int i, char c);

当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件:

 inline
 void f(int i, char c)
 {
   // ...
 }

注意:将函数的定义({...}之间的部分)放在头文件中是强制的,除非该函数仅仅被单个 .cpp 文件使用。尤其是,如果你将内联函数的定义放在 .cpp 文件中并且在其他 .cpp文件中调用它,连接器将给出 “unresolved external” 错误。

9.7 如何告诉编译器使一个成员函数成为内联函数?

声明内联成员函数看上去和普通成员函数非常类似:

 class Fred {
 public:
   void f(int i, char c);
 };

但是当你定义内联成员函数时,在成员函数定义前加上 inline 关键字,并且将定义放入头文件中:

 inline
 void Fred::f(int i, char c)
 {
   // ...
 }

通常将函数的定义({...}之间的部分)放在头文件中是强制的,除非函数只在一个.cpp文件中用到。特别 是,如果你将内联函数的定义放在 .cpp 文件中并且在其他 .cpp 文件中调用它,连接器将给出 “unresolved external”错误。

9.8 有其它方法告诉编译器使成员函数成为内联吗?

有:在类体内定义成员函数:

 class Fred {
 public:
   void f(int i, char c)
     {
       // ...
     }
 };

尽管这对于写类的人来说很容易,但由于它将类是“什么”(what)和类“如何”(how)工作混在一起,给阅读的人带来了困难。我们通常更愿意在类体外使用 inline 关键字定义成员函数来避免这种混合。这种感觉所基于的认识是:在一个面向重用的世界中,使用你的类的人有很多,而编写它的人只有一个(你自己);因此你做任何事都应该照顾多数而不是少数。下一条FAQ进一步应用了这个方法。

9.9 在定义于类外部的内联函数中,以下哪种方法最好:是把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
 { ... }

这里是基本的想法:

  • 类的public部分是你描述类的可见语义的地方,包含公有成员函数、友元函数和任何其它暴露给外部的内容。不要提供在调用者代码中看不到的细节。
  • 类的其它部分,包括非公有部分、成员定义和友元函数声明等等,这些纯粹是实现细节。如果还没有在类的公有部分描述,那么不要提供相关可见语义。

从一种实际的观点来看,这种隔离能够使用户更轻松和更安全。假设Chuck只是想“用”你的类。因为你读了本FAQ并使用了上述隔离办 法,Chuck能够在类的公有部分找到所有需要的内容,而不必看任何不需要的内容。他能够更加轻松,因为只需要看一个地方。同时也会更安全,因为他纯洁的 思想不必受到实现细节的干扰。

回到内联上来:一个函数是否内联只是实现细节,不会改变函数调用的可见语义(即含义)。因此inline关键字应该和函数定义放在一起,而不是在类的public声明区。

注意:大部分人使用“声明”和“定义”来区分以上所述的两个位置。例如,人们会说“我应该把inline关键字放到声明那里还是放在定义那里?”但 这种说法不太严密,可能会有人因此笑话你。笑话你的人可能只是不自信而又装腔作势的可怜虫,他们无法在其生命中取得一些成就。然而,你还是可以学会使用正 确的术语来避免被笑话。其实,每个定义同时也是声明。也就是说,如果把这两者当作是互斥的,那么就好像是在问钢和金属哪个更重。当你把“定义”说成是“声 明”的对立面时,几乎所有人都都明白你的意思。只有最糟糕的痴迷于技术的小人物才会因此嘲笑你。但至少你知道如何正确使用术语。