Oracle Solaris Studio 12.2:C++ 用户指南

第 9 章 改善程序性能

采用编译器易于编译优化的方式编写函数,可以改善 C++ 函数的性能。很多书中都对软件性能做了一般性介绍并具体介绍了 C++,本章不再重复这类重要信息,而只讨论那些显著影响 C++ 编译器的性能技术。

9.1 避免临时对象

C++ 函数经常会产生必须创建并销毁的隐式临时对象。对于重要的类,临时对象的创建和销毁会占用很多处理时间和内存。C++ 编译器消除了某些临时类,但是并不能消除所有的临时类。

您编写的函数要将临时对象的数目减少到理解程序所需的最小数目。这些技术包括:使用显式变量而不使用隐式临时对象,以及使用引用变量而不使用值参数。另外一种技术是实现和使用诸如 += 这样的操作,而非实现和使用只包含 += 的操作。例如,下面的第一行引入了用于保存 a + b 结果的临时对象,而第二行则不是。


T x = a + b;
T x(a); x += b;

9.2 使用内联函数

使用扩展内联而不使用正常调用时,对小而快速的函数的调用可以更小更快速。反过来,如果使用扩展内联而不建立分支,则对又长又慢的函数的调用会更大更慢。另外,只要函数定义更改,就必须重新编译对内联函数的所有调用。因此,使用内联函数时要格外小心。

如果预计函数定义会更改而且重新编译所有调用程序非常耗时,请不要使用内联函数。而如果扩展函数内联的代码比调用函数的代码少,使用函数内联时应用程序执行速度显著提高,则可以使用内联函数。

编译器不能内联所有函数调用,因此要充分利用函数内联,可能需要进行一些源码更改。可使用 +w 选项了解何时不会进行函数内联。在以下情况中,编译器将不会内联函数:

9.3 使用缺省运算符

如果类定义不声明无参数的构造函数、复制构造函数、复制赋值运算符或析构函数,那么编译器将隐式声明它们。它们都是调用的缺省运算符。类似 C 的结构具有这些缺省运算符。编译器生成缺省运算符时,可以了解大量关于需要处理的工作和可以产生优良代码的工作。这种代码通常比用户编写的代码的执行速度快,原因是编译器可以利用汇编级功能的优点,而程序员则不能利用该功能的优点。因此缺省运算符执行所需的工作时,程序不能声明这些运算符的用户定义版本。

缺省运算符是内联函数,因此内联函数不合适时不使用缺省运算符(请参见上一节)。否则,缺省运算符是合适的:

某些 C++ 编程手册建议编写类的程序员始终定义所有的运算符,以便该代码的任何读者都能了解该程序员没有忘记考虑缺省运算符的语义。显然,该建议与以上讨论的优化有冲突。这种冲突的解决方案是在代码中放置注释以表明类正使用缺省运算符。

9.4 使用值类

包括结构和联合在内的 C++ 类通过值来传递和返回。对于 Plain-Old-Data (POD) 类,C++ 编译器需要像 C 编译器一样传递结构。这些类的对象直接进行传递。对于使用了用户定义复制构造函数的类的对象,编译器需要构造对象的副本,将指针传递到副本,并在返回后销毁副本。这些类的对象间接进行传递。编译器也可以选择介于这两个需求之间的类。不过,该选择影响二进制的兼容性,因此编译器对每个类的选择必须保持一致。

对于大多数编译器,直接传递对象可以加快执行速度。这种执行速度的改善对于小值类(例如复数和概率值)来说尤其明显。有时为了改善程序执行效率,您可以设计更可能直接传递而不是间接传递的类。

在兼容模式 (-compat[=4]) 下,如果类具有以下任何一项,则间接进行传递:

否则,类被直接传递。

在标准模式(缺省模式)中,如果类具有以下任何一条,则间接传递该类:

否则,类被直接传递。

9.4.1 选择直接传递类

尽可能直接传递类:

9.4.2 在不同的处理器上直接传递类

C++ 编译器直接传递的类(和联合)与 C 编译器传递结构(或联合)完全相同。不过,C++ 结构和联合在不同的体系结构上进行不同的传递。

表 9–1 在不同体系结构上结构和联合的传递

体系结构  

说明  

SPARC V7/V8 

通过在调用方内分配存储并将指针传递到该存储,传递并返回结构和联合。(也就是说,所有的结构和联合都通过引用传递。) 

SPARC V9 

不超过 16 个字节(32 个字节)的结构在寄存器中传递。通过在调用方内分配存储并将指针传递到该存储,联合和所有其他结构将被传递并返回。(也就是说,小的结构在寄存器中传递,而联合和大的结构通过引用传递。)因此,小值类与基元类具有相同的传递效率。 

x86 平台 

结构和联合通过在堆栈上分配空间并将参数复制到堆栈上来传递。通过在调用程序的帧中分配临时对象并将临时对象的地址作为隐式的第一个参数传递,返回结构和联合。 

9.5 缓存成员变量

访问成员变量是 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;