采用编译器易于编译优化的方式编写函数,可以改善 C++ 函数的性能。有许多关于软件性能的书籍,尤其是关于 C++。例如,Tom Cargill 编著的《 C++ Programming Style》(Addison-Wesley 出版,1992)、Jon Louis Bentley 编著的《 Writing Efficient Programs》(Prentice-Hall 出版,1982)、Dov Bulka 和 David Mayhew 合著的《 Efficient C++: Performance Programming Techniques》(Addison-Wesley 出版,2000)以及 Scott Meyers 编著的《Effective C++ 50 Ways to Improve Your Programs and Designs》第二版(Addison-Wesley 出版,1998)。本章不重复这些有价值的信息,而是讨论了主要影响 C++ 编译器的那些性能技术。
C++ 函数经常会产生必须创建并销毁的隐式临时对象。对于重要的类,临时对象的创建和销毁会占用很多处理时间和内存。C++ 编译器消除了某些临时类,但是并不能消除所有的临时类。
您编写的函数要将临时对象的数目减少到理解程序所需的最小数目。这些技术包括:使用显式变量而不使用隐式临时对象,以及使用引用变量而不使用值参数。另外一种技术是实现和使用诸如 += 这样的操作,而非实现和使用只包含 + 和 = 的操作。例如,下面的第一行引入了用于保存 a + b 结果的临时对象,而第二行则不是。
T x = a + b; T x(a); x += b; |
使用扩展内联而不使用正常调用时,对小而快速的函数的调用可以更小更快速。反过来,如果使用扩展内联而不建立分支,则对又长又慢的函数的调用会更大更慢。另外,只要函数定义更改,就必须重新编译对内联函数的所有调用。因此,使用内联函数时要格外小心。
如果预计函数定义会更改而且重新编译所有调用程序非常耗时,请不要使用内联函数。而如果扩展函数内联的代码比调用函数的代码少,或使用函数内联时应用程序执行速度显著提高,则可以使用内联函数。
编译器不能内联所有函数调用,因此要充分利用函数内联,可能需要进行一些源码更改。可使用 +w 选项了解何时不会进行函数内联。在以下情况中,编译器将不会内联函数:
函数包含了复杂控制构造,例如循环、switch 语句和 try/catch 语句。这些函数很少多次执行复杂控制构造。要内联这种函数,请将函数分割为两部分,里边的部分包含了复杂控制构造,而外边的部分决定了是否调用里边的部分。即使编译器可以内联完整函数,从函数常用部分中分隔出不常用部分的这种技术也可以改善性能。
内联函数体又大又复杂。因为对函数体内其他内联函数的调用,或因为隐式构造函数和析构函数调用(通常发生在派生类的构造函数和析构函数中),所以简单函数体可以非常复杂。对于这种函数,内联扩展很少提供显著的性能改善,所以函数一般不内联。
内联函数调用的参数既大又复杂。对于内联成员函数调用的对象是内联函数调用的自身这种情况,编译器特别敏感。要内联具有复杂参数的函数,只需将函数参数计算到局部变量并将变量传递到函数。
如果类定义不声明无参数的构造函数、复制构造函数、复制赋值运算符或析构函数,那么编译器将隐式声明它们。它们都是调用的缺省运算符。类似 C 的结构具有这些缺省运算符。编译器生成缺省运算符时,可以了解大量关于需要处理的工作和可以产生优良代码的工作。这种代码通常比用户编写的代码的执行速度快,原因是编译器可以利用汇编级功能的优点,而程序员则不能利用该功能的优点。因此缺省运算符执行所需的工作时,程序不能声明这些运算符的用户定义版本。
缺省运算符是内联函数,因此内联函数不合适时不使用缺省运算符(请参见上一节)。否则,缺省运算符是合适的:
用户编写的无参数构造函数仅为构造函数的基对象和成员变量调用无参数构造函数。有效的基元类型具有“不进行任何操作”的无参数构造函数。
用户编写的复制构造函数仅复制所有的基对象和成员变量。
用户编写的复制赋值运算符仅复制所有的基对象和成员变量。
用户编写的析构函数可以为空。
某些 C++ 编程手册建议编写类的程序员始终定义所有的运算符,以便该代码的任何读者都能了解该程序员没有忘记考虑缺省运算符的语义。显然,该建议与以上讨论的优化有冲突。这种冲突的解决方案是在代码中放置注释以表明类正使用缺省运算符。
包括结构和联合在内的 C++ 类通过值来传递和返回。对于 Plain-Old-Data (POD) 类,C++ 编译器需要像 C 编译器一样传递结构。这些类的对象直接进行传递。对于用户定义复制构造函数的类的对象,编译器需要构造对象的副本,将指针传递到副本,并在返回后销毁副本。这些类的对象间接进行传递。编译器也可以选择介于这两个需求之间的类。不过,该选择影响二进制的兼容性,因此编译器对每个类的选择必须保持一致。
对于大多数编译器,直接传递对象可以加快执行速度。这种执行速度的改善对于小值类(例如复数和概率值)来说尤其明显。有时为了改善程序执行效率,您可以设计更可能直接传递而不是间接传递的类。
在兼容模式 (-compat[=4]) 下,如果类具有以下任何一项,则间接进行传递:
用户定义的构造函数
虚函数
虚拟基类
间接传递的基
间接传递的非静态数据成员
否则,类被直接传递。
在标准模式(缺省模式)中,如果类具有以下任何一条,则间接传递该类:
用户定义的复制构造函数
用户定义的析构函数
间接传递的基
间接传递的非静态数据成员
否则,类被直接传递。
尽可能直接传递类:
只要可能,就使用缺省构造函数,尤其是缺省复制构造函数。
尽可能使用缺省析构函数。缺省析构函数不是虚拟的,因此具有缺省析构函数的类通常不是基类。
避免使用虚函数和虚拟基。
C++ 编译器直接传递的类(和联合)与 C 编译器传递结构(或联合)完全相同。不过,C++ 结构和联合在不同的体系结构上进行不同的传递。
表 10–1 在不同体系结构上结构和联合的传递
体系结构 |
说明 |
---|---|
SPARC V7/V8 |
通过在调用方内分配存储并将指针传递到该存储,传递并返回结构和联合。(也就是说,所有的结构和联合都通过引用传递。) |
SPARC V9 |
不超过 16 个字节(32 个字节)的结构在寄存器中传递。通过在调用方内分配存储并将指针传递到该存储,联合和所有其他结构将被传递并返回。(也就是说,小的结构在寄存器中传递,而联合和大的结构通过引用传递。)因此,小值类与基元类具有相同的传递效率。 |
x86 平台 |
结构和联合通过在堆栈上分配空间并将参数复制到堆栈上来传递。通过在调用程序的帧中分配临时对象并将临时对象的地址作为隐式的第一个参数传递,返回结构和联合。 |
访问成员变量是 C++ 成员函数的通用操作。
编译器必须经常通过 this 指针从内存装入成员变量。因为值通过指针装入,所以编译器有时不能决定何时执行第二次装入或以前装入的值是否仍然有效。在这些情况下,编译器必须选择安全但缓慢的方法,在每次访问成员变量时重新装入成员变量。
如下所示,可以通过在局部变量中显式缓存成员变量的值来避免不必要的内存重新装入:
声明局部变量并使用成员变量的值初始化该变量。
在函数中成员变量的位置使用局部变量。
如果局部变量变化,那么将局部变量的最终值赋值到成员变量。不过,如果成员函数在该对象上调用另一个成员函数,那么该优化会产生不可预料的结果。
当值位于寄存器中时,这种优化最有效,而这种情况也与基元类型相同。基于内存的值的优化也会很有效,因为减少的别名使编译器获得了更多的机会来进行优化。
如果成员变量经常通过引用(显式或隐式)来传递,那么优化可能并没什么效果。
有时,类的目标语义需要成员变量的显式缓存,例如在当前对象和其中一个成员函数参数之间有潜在别名时。例如:
complex& operator*= (complex& left, complex& right) { left.real = left.real * right.real + left.imag * right.imag; left.imag = left.real * right.imag + left.image * right.real; } |
会产生不可预料的结果,前提是调用时使用:
x*=x; |