程序性能优化

作者: | 更新日期:

最近在写高访问量服务, 记录一些各种优化技巧

本文首发于公众号:天空的代码世界,微信号:tiankonguse

值传递与引用传递

特点: 构造函数开销

函数的参数值传递时, 会构造一个临时变量,并调用构造函数, 而引用传递时, 不存在临时变量调用构造函数的花销的问题。
对于基本类型, 一般小于8字节, 值传递不消耗多少内存与时间,所以一般值传递。
而对于复杂的结果, 则需要申请内存并初始化,会消耗大量内存和时间, 所以一般引用传递,或者指针传递。

自增问题

特点: 构造函数开销

变量可以前自增(++i)和后自增(i++)。
前自增是变量先增加, 然后返回变量自身的引用。过程中不存在临时变量。
后自增是先把自身的值保存在临时变量中, 然后自身增加, 最后返回临时变量的值。过程中存在两个临时变量。

所以对于基本类型,使用哪个都可以, 但是对于重载了自增符号的复杂的数据结构, 能使用前自增的最好不要使用后自增。

循环中的临时变量

特点: 构造函数开销

可以先看下面的代码。

SomeThing tmpOuter;
for(int i = 0; i < 100; ++i){
    tmpOuter = value;
    //do something
}

for(int i = 0; i < 100; ++i){
    SomeThing tmpInner = value;
    //do something
}

有人会想上面两种代码哪个更好, 实际上两种都不好,尽量写成下面的风格。

for(int i = 0; i < 100; ++i){
    SomeThing& tmpInner = value;
    //do something
}

如果只比较上面的两种代码的话, 需要根据 SomeThing 结构的实际情况来决定。

第一个代码定义变量的时候,初始化一次, 循环中都是赋值。
第二个代码每次循环都调用赋值构造函数和析构函数。

不能使用引用的情况下, 就需要判断这两种情况哪种代价更小了。

复制初始化

特点: 构造函数开销

与直接初始化对应的是复制初始化,什么是直接初始化?什么又是复制初始化?举个简单的例子,

SomeThing tmp;
SomeThing tmp2(tmp);    //直接初始化
SomeThing tmp3 = tmp;    //复制初始化

那么直接初始化与复制初始化又有什么不同呢?
直接初始化是直接以一个对象来构造另一个对象,复制初始化是先构造一个对象,再把另一个对象值复制给这个对象。
从这里,可以看出直接初始化的效率更高一点,而且使用直接初始化还是一个好处,就是对于不能进行复制操作的对象,如流对象,是不能使用赋值初始化的,只能进行直接初始化。

平常的复制初始化通常会被编译器优化,效果和直接初始化一样。但是编译时,必须满足编译前的语法,及允许赋值。

除法

特点: 计算开销

无论是整数还是浮点数运算,除法都是一件运算速度很慢的指令,在计算机中实现除法是比较复杂的。

所以要减少除法运算的次数,下面介绍一些简单方法来提高效率:

  1. 通过数学的方法,把除法变为乘法运算或者位运算,如if(a > b/c),如果a、b、c都是正数,则可写成if(a*c > b)
  2. 让编译器有优化的余地,如里你要做的运算是int型的n/7的话,写成(unsigned)n/7有利于编译器的优化。而要让编译器有优化的余地,则除数必须为常数,而这也可以用const修饰一个变量来达到目的。

内联函数

特点: 函数调用开销

正如我们所知,调用函数是需要保护现场,为局部变量分配内存,函数结束后还要恢复现场等开销,而内联函数则是把它的代码直接写到调用函数处,所以不需要这些开销,但会使程序的源代码长度变大。

所以若是小粒度的函数,内联之后由于不需要调用普通函数的开销,所以可以提高程序的效率。

大函数问题

特点: 一级cache加载开销

由于CPU只能从内存在读取数据,而CPU的运算速度远远大于内存,所以为了提高程序的运行速度有效地利用CPU的能力,在内存与CPU之间有一个叫Cache的存储器,它的速度接近CPU。而Cache中的数据是从内存中加载而来的,这个过程需要访问内存,速度较慢。

这里先说说Cache的设计原理,就是时间局部性和空间局部性。
时间局部性是指如果一个存储单元被访问,则可能该单元会很快被再次访问,这是因为程序存在着循环。
空间局部性是指如果一个储存单元被访问,则该单元邻近的单元也可能很快被访问,这是因为程序中大部分指令是顺序存储、顺序执行的,数据也一般也是以向量、数组、树、表等形式簇聚在一起的。

局部变量-静态变量

特点: 一级cache加载开销

很多人认为局部变量在使用到时才会在内存中分配储存单元,而静态变量在程序的一开始便存在于内存中,所以使用静态变量的效率应该比局部变量高,其实这是一个误区,使用局部变量的效率比使用静态变量要高。

这是因为局部变量是存在于堆栈中的,对其空间的分配仅仅是修改一次esp寄存器的内容即可(即使定义一组局部变量也是修改一次)。
而局部变量存在于堆栈中最大的好处是,函数能重复使用内存,当一个函数调用完毕时,退出程序堆栈,内存空间被回收,当新的函数被调用时,局部变量又可以重新使用相同的地址。
当一块数据被反复读写,其数据会留在CPU的一级缓存(Cache)中,访问速度非常快。而静态变量却不存在于堆栈中。

可以说静态变量是低效的。

多重继承

在C++中,支持多继承,即一个子类可以有多个父类。书上都会跟我们说,多重继承的复杂性和使用的困难,并告诫我们不要轻易使用多重继承。其实多重继承并不仅仅使程序和代码变得更加复杂,还会影响程序的运行效率。

这是因为在C++中每个对象都有一个this指针指向对象本身,而C++中类对成员变量的使用是通过this的地址加偏移量来计算的,而在多重继承的情况下,这个计算会变量更加复杂,从而降低程序的运行效率。而为了解决二义性,而使用虚基类的多重继承对效率的影响更为严重,因为其继承关系更加复杂和成员变量所属的父类关系更加复杂。

dynamic_cast

dynamic_cast的作用是进行指针或引用的类型转换,dynamic_cast的转换需要目标类型和源对象有一定的关系:继承关系。
实现从子类到基类的指针转换,实际上这种转换是非常低效的,对程序的性能影响也比较大,不可大量使用,而且继承关系越复杂,层次越深,其转换时间开销越大。在程序中应该尽量减少使用。

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越