C++程序加速的12个⽅法
C++程序加速的12个⽅法
⽂章⽬录
1.将反复使⽤的数据存放在全局变量⾥⾯。
需要重复使⽤的数据,⽐如加载的图⽚,CSV⽂件等数据,存放在全局变量⾥⾯,每次加载DLL时,只加载⼀次,直到卸载DLL,这些数据⼀直保持在内存中,避免重复加载,经过测试,这样处理之后,漏装检测的时间由2.5S降低到了1.5S,因为反复读取⽂件,图⽚是⼀个很消耗时间的操作,要尽量避免。
2.使⽤多线程
如果程序中有可以同时进⾏的代码,譬如五道算术题,⼤家每⼈算⼀道,最后只花费1/5的时间就解决了5道题,多线程就是这个思路,现代的CPU都是多核的,就相当于可以同时算五道题⽬,⼀旦你的代码使⽤了多线程,恐怕你再也回不到单线程的时代了。
有⼀段程序花费300ms,我们开了32个多线程之后花费16ms,加速了20倍,更别提GPU动辄百倍的加速效果。
源代码大电影3.⽤a++和++a,a–,--a
通常使⽤⾃加、⾃减指令和复合赋值表达式(如a-=1及a+=1等)都能够⽣成⾼质量的程序代码,编译器通常都能够⽣成inc和dec之类的指令,⽽使⽤a=a+1或a=a-1之类的指令,有很多C编译器都会⽣成⼆到三个字节的指令。
4.减少除法运算
⽆论是整数还是浮点数的运算,除法都会⽐较耗时,所以最后将除法运算等效成乘法运算。例如:a/b>20可改为a>b*20,可以简化程序的运⾏时间。
5.尽量减少值传递,多⽤引⽤来传递参数。
⾄于其中的原因,相信⼤家也很清楚,如果参数是int等语⾔⾃定义的类型可能能性能的影响还不是很⼤,但是如果参数是⼀个类的对象,那么其效率问题就不⾔⽽喻了。例如⼀个判断两个字符串是否相等的函数,其声明如下:
bool Compare(string s1, string s2)
bool Compare(string *s1, string *s2)
bool Compare(string &s1, string &s2)
bool Compare(const string &s1, const string &s2)
其中若使⽤第⼀个函数(值传递),则在参数传递和函数返回时,需要调⽤string的构造函数和析构函数两次(即共多调⽤了四个函数),⽽其他的三个函数(指针传递和引⽤传递)则不需要调⽤这四个函数。因为指针和引⽤都不会创建新的对象。如果⼀个构造⼀个对象和析构⼀个对象的开销是庞⼤的,这就是会效率造成⼀定的影响。
然⽽在很多⼈的眼中,指针是⼀个恶梦,使⽤指针就意味着错误,那么就使⽤引⽤吧!它与使⽤普通值传递⼀样⽅便直观,同时具有指针传递的⾼效和能⼒。因为引⽤是⼀个变量的别名,对其操作等同于对实际对象操作,所以当你确定在你的函数是不会或不需要变量参数的值时,就⼤胆地在声明的前⾯加上⼀个const吧,就如最后的⼀个函数声明⼀样。
同时加上⼀个const还有⼀个好处,就是可以对常量进⾏引⽤,若不加上const修饰符,引⽤是不能引⽤常量的。
举个例⼦,我们使⽤值传递和指针传递的⽅式传递⼀张10M的图⽚,使⽤值传递会多花费0.1S的时间。假如我们使⽤了10次值传递,将会增加1S的运⾏时间。
6.循环引发的讨论1(循环内定义,还是循环外定义对象)
请看下⾯的两段代码:
代码1:
ClassTest CT;
for(inti = 0; i < 100; ++i)
{
CT = a;
//do something
}
代码2:
for(inti = 0; i < 100; ++i)
{
ClassTest CT = a;
//do something
}
你会觉得哪段代码的运⾏效率较⾼呢?代码1科学家是代码2?其实这种情况下,哪段代码的效率更⾼是不确定的,或者说是由这个类ClassTest本向决定的,分析如下:
对于代码1:需要调⽤ClassTest的构造函数1次,赋值操作函数(operator=)100次;对于代码2:需要⾼⽤(复制)构造函数100次,析构函数100次。
如果调⽤赋值操作函数的开销⽐调⽤构造函数和析构函数的总开销⼩,则第⼀种效率⾼,否则第⼆种的效率⾼。
⼤部分情况下我们建议循环外定义,也就是⽅法⼀
7.循环引发的讨论2(避免过⼤的循环)
现在请看下⾯的两段代码,
代码1:
for(inti = 0; i < n; ++i)
{
fun1();
fun2();
}
代码2:
for(inti = 0; i < n; ++i)
{
fun1();
}
for(inti = 0; i < n; ++i)
{
fun2();
}
注:这⾥的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次即可。
8.局部变量VS静态变量
很多⼈认为局部变量在使⽤到时才会在内存中分配,也就是储存单元,⽽静态变量在程序的⼀开始便存在于内存中,所以使⽤静态变量的效率应该⽐局部变量⾼,其实这是⼀个误区,使⽤局部变量的效
率⽐使⽤静态变量要⾼。
这是因为局部变量是存在于堆栈中的,对其空间的分配仅仅是修改⼀次esp寄存器的内容即可(即使定义⼀组局部变量也是修改⼀次)。⽽局部变量存在于堆栈中最⼤的好处是,函数能重复使⽤内存,当⼀个函数调⽤完毕时,退出程序堆栈,内存空间被回收,当新的函数被调⽤时,局部变量⼜可以重新使⽤相同的地址。当⼀块数据被反复读写,其数据会留在CPU的⼀级缓存(Cache)中,访问速度⾮常快。⽽静态变量却不存在于堆栈中。
可以说静态变量是低效的
9.避免使⽤多重继承
在C++中,⽀持多继承,即⼀个⼦类可以有多个⽗类。书上都会跟我们说,多重继承的复杂性和使⽤的困难,并告诫我们不要轻易使⽤多重继承。其实多重继承并不仅仅使程序和代码变得更加复杂,还会影响程序的运⾏效率。
这是因为在C++中每个对象都有⼀个this指针指向对象本⾝,⽽C++中类对成员变量的使⽤是通过this的地址加偏移量来计算的,⽽在多重继承的情况下,这个计算会变量更加复杂,从⽽降低程序的运⾏效率。⽽为了解决⼆义性,⽽使⽤虚基类的多重继承对效率的影响更为严重,因为其继承关系更加复杂和成员变量所属的⽗类关系更加复杂。
10.将⼩粒度函数声明为内联函数(inline)
正如我们所知,调⽤函数是需要保护现场,为局部变量分配内存,函数结束后还要恢复现场等开销,⽽内联函数则是把它的代码直接写到调⽤函数处,所以不需要这些开销,但会使程序的源代码长度变⼤。
所以若是⼩粒度的函数,如下⾯的Max函数,由于不需要调⽤普通函数的开销,所以可以提⾼程序的效率。
inline Max(inta, intb)
{
returna>b?a:b;
}
11.多⽤直接初始化
与直接初始化对应的是复制初始化,什么是直接初始化?什么⼜是复制初始化?举个简单的例⼦,
ClassTest ct1;
ClassTest ct2(ct1); //直接初始化
ClassTest ct3 = ct1; //复制初始化
那么直接初始化与复制初始化⼜有什么不同呢?直接初始化是直接以⼀个对象来构造另⼀个对象,如⽤ct1来构造ct2,复制初始化是先构造⼀个对象,再把另⼀个对象值复制给这个对象,如先构造⼀个对象ct3,再把ct1中的成员变量的值复制给ct3,从这⾥,可以看出直接初始化的效率更⾼⼀点,⽽且使⽤直接初始化还是⼀个好处,就是对于不能进⾏复制操作的对象,如流对象,是不能使⽤赋值初始化的,只能进⾏直接初始化。可能我说得不太清楚,那么下⾯就引⽤⼀下经典吧!
以下是Primer是的原话:
“当⽤于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调⽤与实参匹配的构造函数,复制初始化总是调⽤复制构造函数。复制初始化⾸先使⽤指定构造函数创建⼀个临时对象,然后⽤复制构造函数将那个临时对象复制到正在创建的对象”,还有⼀段这样说,“通常直接初始化和复制初始化仅在低级别优化上存在差异,然⽽,对于不⽀持复制的类型,或者使⽤⾮explicit构造函数的时候,它们有本质区别:
ifstream file1("filename")://ok:direct initialization
ifstream file2 = "filename";//error:copy constructor is private
注:如还对直接初始化和复制初始化有疑问,可以参考⼀下前⾯的⼀篇⽂章:
C++直接初始化与复制初始化的区别深⼊解析,⾥⾯有有关直接初始化和复制初始化的详细解释。
12.尽量少使⽤dynamic_cast
dynamic_cast的作⽤是进⾏指针或引⽤的类型转换,dynamic_cast的转换需要⽬标类型和源对象有⼀定的关系:继承关系。 实现从⼦类到基类的指针转换,实际上这种转换是⾮常低效的,对程序的性能影响也⽐较⼤,不可⼤量使⽤,⽽且继承关系越复杂,层次越深,其转换时间开销越⼤。在程序中应该尽量减少使⽤。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论