提高程序运行效率的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的地址加偏移量来计算的,而在多重继承的情况下,这个计算会变量更加复杂,从而降低程序的运行效率。而为了解决二义性,而使用虚基类的多重继承对效率的影响更为严重,因为其继承关系更加复杂和成员变量所属的父类关系更加复杂。
 
 
 

猜你喜欢

转载自www.cnblogs.com/huanian/p/12760683.html