提高程序运行效率的6个简单方法(3)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
提⾼程序运⾏效率的6个简单⽅法(3)
注:以C/C++为例。
⼀、尽量减少使⽤值传递⽅式,多使⽤引⽤传递⽅式。
如果传递的参数是int等基本数据类型,可能对性能的影响还不是很⼤,但是如果传递的参数是⼀个类的对象,那么其效率问题就不⾔⽽喻了。
例如:⼀个判断两个字符串是否相等的函数,其声明如下:
1bool Compare(string s1, string s2)
2bool Compare(string *s1, string *s2)
3bool Compare(string &s1, string &s2)
4bool 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;
4return *this;
5 }
6 _SingleList::Iterator _SingleList::Iterator::operator++(int)//i++
7 {
8 Iterator tmp(*this);
9 pNote = pNote->pNext;
10return tmp;
11 }
从i++ 的实现⽅式可以知道,对象利⽤⾃⼰创建⼀个临时对象(将类的所有属性复制⼀份),然后改变⾃⼰的状态,并返回所创建的临时对象;⽽++i 的实现⽅式时,直接改变⾃⼰的内部状态,并返回⾃⼰的引⽤。
从第⼀点的论述可以知道 i++ 实现⽅式,创建对象时会调⽤构造函数,在函数返回时还要调⽤析构函数,⽽由于 ++i 实现⽅式直接改变对象的内部状态,并返回⾃⼰的引⽤,⾄始⾄终也没有创建新的对象,所以也就不会调⽤构造函数和析构函数。
然⽽更加糟糕的是,迭代器通常是⽤来遍历容器的,它⼤多应⽤在循环中,试想你的链表有100个元素,⽤下⾯的两种⽅式遍历:
1for(_SingleList::Iterator it = list.begin(); it != list.end(); ++i)
2 {
3//do something
4 }
5
6for(_SingleList::Iterator it = list.begin(); it != list.end(); i++)
7 {
8//do something
9 }
如果你的习惯不好,写了第⼆种形式,那么很不幸,做同样的事情,就是因为⼀个 ++i 和⼀个 i++ 的区别,你就要调⽤多200个函数(构造和析构函数),其对效率的影响可就不可忽视了。
三、循环引发的讨论1。
(循环内定义,还是循环外定义对象)
请看下⾯的两段代码:
1 //代码1:
2 ClassTest CT;
3for(int i = 0; i < 100; ++i)
4 {
5 CT = a;
6//do something
7 }
8 //代码2:
9for(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:
2for(int i = 0; i < n; ++i)
3 {
4 fun1();
5 fun2();
6 }
7
8//代码2:
9for(int i = 0; i < n; ++i)
10 {
11 fun1();
12 }
13for(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的地址加偏移量来计算的,⽽在多重继承的情况下,这个计算会变量更加复杂,从⽽降低程序的运⾏效率。
⽽为了解决⼆义性,⽽使⽤虚基类的多重继承对效率的影响更为严重,因为其继承关系更加复杂和成员变量所属的⽗类关系更加复杂。