八二法则
八二法则是说软件的整体性能几乎总是由代码的一小部分决定。80%的资源用在20%代码上、80%的内存被20%的代码使用、80%的磁盘访问被20%的代码使用等。
找出影响效率的那20%的代码的错误方式就是靠直觉来猜测,而程序的性能特质倾向于高度的非直觉性,最有效的办法还是根据观察或者实验结果来分析识别出造成效率低下的那20%的部分。
采用缓式计算
原则:在你真正需要之前,不必着急为某物做一个副本,而是用拖延战术,只要可以,就使用其他的副本。
区分读写
在linux中创建进程采用fork,其中有个叫copy on write的技术,也就是fork之后不会马上复制父进程的所有资源,而是子进程共享父进程的资源,直到写那一刻才拥有自己的副本。这样就加快了子进程创建速度,因为大部分情况创建进程可能会调用exec来调用新的程序来执行。所以有时候复制是徒劳的。
缓式取出
对于大的对象而言,产生该对象时只是产生一个壳子,其中的成员变量(可能从磁盘读取,可能从网络读取,可能从数据库读取)的初始化只有用到该变量时才进行初始化。如果万一用不到某个变量,我们也就节省了一部分初始化的时间。
缓式计算
对于一些大型的运算(如大的矩阵运算),可以先记录数值,等到被使用时才进行计算。
小节
对于一定要进行的计算,缓式计算并不获得太多的好处,终究需要计算,反而因此浪费更多的空间和时间。但是如果有些计算是不必要的,可以省略的,则缓式计算可以提高效率。
采用超急评估
该策略与缓式评估相反,其背后的观念是:如果预期程序会常常用到某个运算,则设法降低每次计算的平均成本,办法就是设计一个数据结构以便能够有效的处理需求。
使用Caching
使用Cache的简单做法就是将“已经计算好而有可能再次被需要”的数据保存下来。
比如程序对数据库中的某个字段需要频繁查询,最好的办法就是,首次查询到某个字段就存储在内存的一个数据结构中,后边的查询如果发现已经在内存中就不要再去读取数据库。以内存的读取代替数据库的读取,从而降低了每次读取数据的成本。
使用prefetching
在处理器中存在指令缓存和数据缓存。使用这种方式的依据是局部引用原理:如果某处的数据被需要,则通常其邻近的数据也会被需要。所以提前预取指令和数据会分摊每次读取指令和数据的时间成本。
另外一个现象是,在std::vector类存在capacity和size两个方法,capacity返回实际空间,size返回已经使用的空间。vector一般分配稍多的内存用于存储数据,防止vector动态增长时每次都进行系统调用进行内存分配。因为系统调用往往比在进程中的函数调用速度慢,所以尽量少的使用系统调用。
小节
超急评估采用的是传统的采用空间换取时间的策略。
当必须支持某些运算而其结果不总是需要的时候,可使用缓式计算。
当必须支持某些运算而其结果几乎总是被需要的时候,采用超急评估。
了解临时对象
C++所谓的临时对象是不可见的。只要产生了一个non-heap object而没有为它命名,便诞生了一个临时对象。这些匿名对象产生于两种情况:
1. 为了让函数调用成功,对实参实行了隐式类型转换。
2. 函数返回对象的时候。
了解临时对象为什么产生,以及产生销毁所需要的成本将对程序产生的影响是很重要的。
情况1
如果传递某个对象给函数,实参类型与函数的对应形参类型并不匹配,为了让函数调用成功,便产生了临时对象。这种转换的前提条件是函数参数的类型为按值传递或者通过常量引用传递。如果是非常量的引用则不会隐式转换。
消除隐式转换的一种方法是针对需要的类型,重载对应的函数,让编译器去调用最合适的类型函数,这样就免去了隐式转换的过程。
class Int(){
public:
Int(int){}
};
const Int operator+(const lhs& l, const rhs& r)
Int a(3);
Int x = a + 2;//1
Int y = 3 + a;//2
//1和2调用成功的原因是因为发生了隐式转换。可以提供以下重载,防止隐式转换
const Int operator+(const lhs& l, int rhs);
const Int operator+(int lhs, const rhs& r);
情况2
第二种产生临时对象的过程是函数返回一个对象。如
const operator+(const Number& lhs, const Number& rhs);
函数返回是个对象,没有名称,所以是临时对象。
返回值优化
可以通过某种特殊的写法,使它返回对象时,能够让编译器消除临时对象的成本。使用的技术是:返回所谓的constructor argument以取代对象。
const operator+(const Number& lhs, const Number& rhs){
return Number(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs.denominator());
}
Number a = 10;
Number b = 12;
Number c = a * b;
以上代码直接调用构造函数,直接在c的内存中构造对象,没有任何临时对象被产生出来。
了解虚函数、多重继承、虚基类的成本
虚函数
class vtbl
程序中的每一个class凡是声明或者继承了虚函数的,都有一个自己的vtbl,其中的条目就是该class各个虚函数的实现的指针。所以引出了第一个虚函数的成本:必须为每一个拥有虚函数的类分配一个vtbl空间,大小与其虚函数个数(包含继承而来的)相同。vtbl通常存储在内含第一个non-inline、non-pure虚函数的定义式的目标文件中。object vptr
包含虚函数的类的vtbl必须能够让它产生的对象找到才能使用。所以这个类的每一个对象都包含一个指向vtbl的指针vptr。引出第二个虚函数成本:每个对象必须包含一个指向vtbl的指针。虚函数的调用过程
- 根据对象的vptr找到vtbl
- 找到被调虚函数函数在vtbl内的指针。因为编译器为每个虚函数指定了一个唯一的索引值,所以只需要一个偏移量即可
- 调用步骤2的函数
如果每个对象都含有的隐藏指针为vptr,而函数的在vtbl的所以为i,那么
pC->f();
//等价于
(*pC->vptr[i])(pC); //pC的含义为this,代表对象本身
这和利用指针调用一个非虚函数效率相当。虚函数本身并不构成性能上的瓶颈。
- 虚函数和inline
虚函数是延时绑定,在运行期间根据动态类型调用函数。
inline意味着在编译器对函数调用采用函数本体替换。
引出了虚函数的第三个成本:使用了虚函数相当于放弃了inline。虚函数无法被inline。
多重继承
1.由于存在多个基类,所以子类的class的vtbls会变大,同时对象的vptrs也随着基类的数量而增加。造成了更多空间的占用,运行期的成本也稍有增加。
2.对于多继承中的“菱形形式”如果采用了虚拟基类的方法,为了避免基类的成员在其子类中有多个重复,子类采用了指针的形式,让各个子类都具有一个指向基类数据成员的指针,所以,虚拟基类多重继承还会导致子类对象中隐含指针数量增加。
RTTI
RTTI使得我们可以在运行期获得类或者对象的相关信息。所以需要空间来存储这份信息。其实每个class只需要一份就够了。而这点恰好和class的vtbl相似,所以RTTI的设计理念是:根据class的vtbl来实现。所以对于存在一个虚函数的类,需要一个空间来存放RTTI使用的type_info对象。