提高程序运行效率的6个简单方法(3)
注:以C/C++为例。
一、尽量减少使用值传递方式,多使用引用传递方式。
如果传递的参数是int等基本数据类型,可能对性能的影响还不是很大,但是如果传递的参数是一个类的对象,那么其效率问题就不言而喻了。
例如:一个判断两个字符串是否相等的函数,其声明如下:
1 bool Compare(string s1, string s2) 2 bool Compare(string *s1, string *s2) 3 bool Compare(string &s1, string &s2) 4 bool Compare(const string &s1, const string &s2)
如果调用第一个函数(值传递方式),则在参数传递(函数调用开始)和函数返回时(函数调用结束之前),需要调用string的构造函数和析构函数两次(即共多调用了四个函数)。而其他的三个函数(指针传递和引用传递)则不需要调用这四个函数。因为指针和引用都不会创建新的对象。如果一个构造一个对象和析构一个对象的开销是庞大的,这就是会效率造成一定的影响。
二、++i和i++引申出的效率问题。
对于基本数据类型变量的自增运算:++i 和 i++ 的区别相信大家也是很清楚的。然而,在这里我想跟大家谈的却是C++类的运算符重载,为了与基本数据类型的用法一致,在C++中重载运算符++时一般都会把 ++i 和 i++ 都重载。你可能会说,你在代码中不会使用重载++运算符,但是你敢说你没有使用过类的++运算符重载吗?迭代器类你总使用过吧!可能到现在你还不是很懂我在说什么,那么就先看看下面的例子:
1 _SingleList::Iterator& _SingleList::Iterator::operator++() //++i 2 { 3 pNote = pNote->pNext; 4 return *this; 5 } 6 _SingleList::Iterator _SingleList::Iterator::operator++(int)//i++ 7 { 8 Iterator tmp(*this); 9 pNote = pNote->pNext; 10 return tmp; 11 }
从 i++ 的实现方式可以知道,对象利用自己创建一个临时对象(将类的所有属性复制一份),然后改变自己的状态,并返回所创建的临时对象;而 ++i 的实现方式时,直接改变自己的内部状态,并返回自己的引用。
从第一点的论述可以知道 i++ 实现方式,创建对象时会调用构造函数,在函数返回时还要调用析构函数,而由于 ++i 实现方式直接改变对象的内部状态,并返回自己的引用,至始至终也没有创建新的对象,所以也就不会调用构造函数和析构函数。
然而更加糟糕的是,迭代器通常是用来遍历容器的,它大多应用在循环中,试想你的链表有100个元素,用下面的两种方式遍历:
1 for(_SingleList::Iterator it = list.begin(); it != list.end(); ++i) 2 { 3 //do something 4 } 5 6 for(_SingleList::Iterator it = list.begin(); it != list.end(); i++) 7 { 8 //do something 9 }
如果你的习惯不好,写了第二种形式,那么很不幸,做同样的事情,就是因为一个 ++i 和一个 i++ 的区别,你就要调用多200个函数(构造和析构函数),其对效率的影响可就不可忽视了。
三、循环引发的讨论1。(循环内定义,还是循环外定义对象)
请看下面的两段代码:
1 //代码1: 2 ClassTest CT; 3 for(int i = 0; i < 100; ++i) 4 { 5 CT = a; 6 //do something 7 } 8 //代码2: 9 for(int i = 0; i < 100; ++i) 10 { 11 ClassTest CT = a; 12 //do something 13 }
对于代码1:需要调用ClassTest的构造函数1次,赋值操作函数(operator=)100次;对于代码2:需要调用构造函数100次,析构函数100次。
如果调用赋值操作函数的开销比调用构造函数和析构函数的总开销小,则第一种效率高,否则第二种的效率高。
四、循环引发的讨论2(避免过大的循环)
现在请看下面的两段代码,
1 //代码1: 2 for(int i = 0; i < n; ++i) 3 { 4 fun1(); 5 fun2(); 6 } 7 8 //代码2: 9 for(int i = 0; i < n; ++i) 10 { 11 fun1(); 12 } 13 for(int i = 0; i < n; ++i) 14 { 15 fun2(); 16 }
注:这里的fun1()和fun2()是没有关联的,即两段代码所产生的结果是一样的。
以代码的层面上来看,似乎是代码1的效率更高,因为毕竟代码1少了n次的自加运算和判断,毕竟自加运算和判断也是需要时间的。但是现实真的是这样吗?
这就要看fun1和fun2这两个函数的规模(或复杂性)了,如果这多个函数的代码语句很少,则代码1的运行效率高一些,但是若fun1和fun2的语句有很多,规模较大,则代码2的运行效率会比代码1显著高得多。可能你不明白这是为什么,要说是为什么这要由计算机的硬件说起。
由于CPU只能从内存在读取数据,而CPU的运算速度远远大于内存,所以为了提高程序的运行速度有效地利用CPU的能力,在内存与CPU之间有一个叫Cache的存储器,它的速度接近CPU。而Cache中的数据是从内存中加载而来的,这个过程需要访问内存,速度较慢。
这里先说说Cache的设计原理,就是时间局部性和空间局部性。时间局部性是指如果一个存储单元被访问,则可能该单元会很快被再次访问,这是因为程序存在着循环。空间局部性是指如果一个储存单元被访问,则该单元邻近的单元也可能很快被访问,这是因为程序中大部分指令是顺序存储、顺序执行的,数据也一般也是以向量、数组、树、表等形式簇聚在一起的。
看到这里你可能已经明白其中的原因了。没错,就是这样!如果fun1和fun2的代码量很大,例如都大于Cache的容量,则在代码1中,就不能充分利用Cache了(由时间局部性和空间局部性可知),因为每循环一次,都要把Cache中的内容踢出,重新从内存中加载另一个函数的代码指令和数据,而代码2则更很好地利用了Cache,利用两个循环语句,每个循环所用到的数据几乎都已加载到Cache中,每次循环都可从Cache中读写数据,访问内存较少,速度较快,理论上来说只需要完全踢出fun1的数据1次即可。
五、局部变量 PK 静态变量
很多人认为局部变量在使用到时才会在内存中分配储存单元,而静态变量在程序的一开始便存在于内存中,所以使用静态变量的效率应该比局部变量高,其实这是一个误区。实际上,使用局部变量的效率比使用静态变量要高。
这是因为局部变量是存在于堆栈中的,对其空间的分配仅仅是修改一次esp寄存器的内容即可(即使定义一组局部变量也是修改一次)。而局部变量存在于堆栈中最大的好处是,函数能重复使用内存,当一个函数调用完毕时,退出程序堆栈,内存空间被回收,当新的函数被调用时,局部变量又可以重新使用相同的地址。当一块数据被反复读写,其数据会留在CPU的一级(Cache)中,访问速度非常快。而静态变量却不存在于堆栈中。可以说静态变量是低效的。
六、避免使用多重继承
在C++中,支持多继承,即一个子类可以有多个父类。书上都会跟我们说,多重继承的复杂性和使用的困难,并告诫我们不要轻易使用多重继承。其实,多重继承并不仅仅使程序和代码变得更加复杂,还会影响程序的运行效率。
这是因为在C++中每个对象都有一个this指针指向对象本身,而C++中类对成员变量的使用是通过this的地址加偏移量来计算的,而在多重继承的情况下,这个计算会变量更加复杂,从而降低程序的运行效率。而为了解决二义性,而使用虚基类的多重继承对效率的影响更为严重,因为其继承关系更加复杂和成员变量所属的父类关系更加复杂。