提高C++程序運(yùn)行效率的10個簡單方法
本文以C/C++程序?yàn)槔v述了程序運(yùn)行效率的10個簡單方法,分享給大家供大家參考之用。具體分析如下:
對于每一個程序員來說,程序的運(yùn)行效率都是一個值得重視,并為之付出努力的問題。但是程序性能的優(yōu)化也是一門復(fù)雜的學(xué)問,需要很多的知識,然而并不是每個程序員都具備這樣的知識,而且論述如何優(yōu)化程序提高程序運(yùn)行效率的書籍也很少。但是這并不等于我們可以忽略程序的運(yùn)行效率,下面就介紹一下本人積累的一些簡單實(shí)用的提高程序運(yùn)行效率的方法,希望對大家有所幫助。
一、盡量減少值傳遞,多用引用來傳遞參數(shù)。
至于其中的原因,相信大家也很清楚,如果參數(shù)是int等語言自定義的類型可能能性能的影響還不是很大,但是如果參數(shù)是一個類的對象,那么其效率問題就不言而喻了。例如一個判斷兩個字符串是否相等的函數(shù),其聲明如下:
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)
其中若使用第一個函數(shù)(值傳遞),則在參數(shù)傳遞和函數(shù)返回時,需要調(diào)用string的構(gòu)造函數(shù)和析構(gòu)函數(shù)兩次(即共多調(diào)用了四個函數(shù)),而其他的三個函數(shù)(指針傳遞和引用傳遞)則不需要調(diào)用這四個函數(shù)。因?yàn)橹羔樅鸵枚疾粫?chuàng)建新的對象。如果一個構(gòu)造一個對象和析構(gòu)一個對象的開銷是龐大的,這就是會效率造成一定的影響。
然而在很多人的眼中,指針是一個惡夢,使用指針就意味著錯誤,那么就使用引用吧!它與使用普通值傳遞一樣方便直觀,同時具有指針傳遞的高效和能力。因?yàn)橐檬且粋€變量的別名,對其操作等同于對實(shí)際對象操作,所以當(dāng)你確定在你的函數(shù)是不會或不需要變量參數(shù)的值時,就大膽地在聲明的前面加上一個const吧,就如最后的一個函數(shù)聲明一樣。
同時加上一個const還有一個好處,就是可以對常量進(jìn)行引用,若不加上const修飾符,引用是不能引用常量的。
二、++i和i++引申出的效率問題
看了上面的第一點(diǎn),你可能覺得,那不就是多調(diào)用了四個函數(shù)而已,你可能對此不屑一顧。那么來看看下面的例子,應(yīng)該會讓你大吃一驚。
至于整型變量的前加和后加的區(qū)別相信大家也是很清楚的。然而在這里我想跟大家談的卻是C++類的運(yùn)算符重載,為了與整形變量的用法一致,在C++中重載運(yùn)算符++時一般都會把前加和后加都重載。你可能會說,你在代碼中不會重載++運(yùn)算符,但是你敢說你沒有使用過類的++運(yùn)算符重載嗎?迭代器類你總使用過吧!可能到現(xiàn)在你還不是很懂我在說什么,那么就先看看下面的例子吧,是本人為鏈表寫的一個內(nèi)部迭代器。
_SingleList::Iterator& _SingleList::Iterator::operator++()//前加 { pNote = pNote->pNext; return *this; } _SingleList::Iterator _SingleList::Iterator::operator++(int)//后加 { Iterator tmp(*this); pNote = pNote->pNext; return tmp; }
從后加的實(shí)現(xiàn)方式可以知道,對象利用自己創(chuàng)建一個臨時對象(自己在函數(shù)調(diào)用的一個復(fù)制),然后改變自己的狀態(tài),并返回這個臨時對象,而前加的實(shí)現(xiàn)方式時,直接改變自己的內(nèi)部狀態(tài),并返回自己的引用。
從第一點(diǎn)的論述可以知道后加實(shí)現(xiàn)時會調(diào)用復(fù)制構(gòu)造函數(shù),在函數(shù)返回時還要調(diào)用析構(gòu)函數(shù),而由于前加實(shí)現(xiàn)方式直接改變對象的內(nèi)部狀態(tài),并返回自己的引用,至始至終也沒有創(chuàng)建新的對象,所以也就不會調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)。
然而更加糟糕的是,迭代器通常是用來遍歷容器的,它大多應(yīng)用在循環(huán)中,試想你的鏈表有100個元素,用下面的兩種方式遍歷:
for(_SingleList::Iterator it = list.begin(); it != list.end(); ++it) { //do something } for(_SingleList::Iterator it = list.begin(); it != list.end(); it++) { //do something }
如果你的習(xí)慣不好,寫了第二種形式,那么很不幸,做同樣的事情,就是因?yàn)橐粋€前加和一個后加的區(qū)別,你就要調(diào)用多200個函數(shù),其對效率的影響可就不可忽視了。
三、循環(huán)引發(fā)的討論1(循環(huán)內(nèi)定義,還是循環(huán)外定義對象)
請看下面的兩段代碼:
代碼1:
ClassTest CT; for(int i = 0; i < 100; ++i) { CT = a; //do something }
代碼2:
for(int i = 0; i < 100; ++i) { ClassTest CT = a; //do something }
你會覺得哪段代碼的運(yùn)行效率較高呢?代碼1科學(xué)家是代碼2?其實(shí)這種情況下,哪段代碼的效率更高是不確定的,或者說是由這個類ClassTest本向決定的,分析如下:
對于代碼1:需要調(diào)用ClassTest的構(gòu)造函數(shù)1次,賦值操作函數(shù)(operator=)100次;對于代碼2:需要高用(復(fù)制)構(gòu)造函數(shù)100次,析構(gòu)函數(shù)100次。
如果調(diào)用賦值操作函數(shù)的開銷比調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)的總開銷小,則第一種效率高,否則第二種的效率高。
四、循環(huán)引發(fā)的討論2(避免過大的循環(huán))
現(xiàn)在請看下面的兩段代碼,
代碼1:
for(int i = 0; i < n; ++i) { fun1(); fun2(); }
代碼2:
for(int i = 0; i < n; ++i) { fun1(); } for(int i = 0; i < n; ++i) { fun2(); }
注:這里的fun1()和fun2()是沒有關(guān)聯(lián)的,即兩段代碼所產(chǎn)生的結(jié)果是一樣的。
以代碼的層面上來看,似乎是代碼1的效率更高,因?yàn)楫吘勾a1少了n次的自加運(yùn)算和判斷,畢竟自加運(yùn)算和判斷也是需要時間的。但是現(xiàn)實(shí)真的是這樣嗎?
這就要看fun1和fun2這兩個函數(shù)的規(guī)模(或復(fù)雜性)了,如果這多個函數(shù)的代碼語句很少,則代碼1的運(yùn)行效率高一些,但是若fun1和fun2的語句有很多,規(guī)模較大,則代碼2的運(yùn)行效率會比代碼1顯著高得多??赡苣悴幻靼走@是為什么,要說是為什么這要由計(jì)算機(jī)的硬件說起。
由于CPU只能從內(nèi)存在讀取數(shù)據(jù),而CPU的運(yùn)算速度遠(yuǎn)遠(yuǎn)大于內(nèi)存,所以為了提高程序的運(yùn)行速度有效地利用CPU的能力,在內(nèi)存與CPU之間有一個叫Cache的存儲器,它的速度接近CPU。而Cache中的數(shù)據(jù)是從內(nèi)存中加載而來的,這個過程需要訪問內(nèi)存,速度較慢。
這里先說說Cache的設(shè)計(jì)原理,就是時間局部性和空間局部性。時間局部性是指如果一個存儲單元被訪問,則可能該單元會很快被再次訪問,這是因?yàn)槌绦虼嬖谥h(huán)??臻g局部性是指如果一個儲存單元被訪問,則該單元鄰近的單元也可能很快被訪問,這是因?yàn)槌绦蛑写蟛糠种噶钍琼樞虼鎯?、順序?zhí)行的,數(shù)據(jù)也一般也是以向量、數(shù)組、樹、表等形式簇聚在一起的。
看到這里你可能已經(jīng)明白其中的原因了。沒錯,就是這樣!如果fun1和fun2的代碼量很大,例如都大于Cache的容量,則在代碼1中,就不能充分利用Cache了(由時間局部性和空間局部性可知),因?yàn)槊垦h(huán)一次,都要把Cache中的內(nèi)容踢出,重新從內(nèi)存中加載另一個函數(shù)的代碼指令和數(shù)據(jù),而代碼2則更很好地利用了Cache,利用兩個循環(huán)語句,每個循環(huán)所用到的數(shù)據(jù)幾乎都已加載到Cache中,每次循環(huán)都可從Cache中讀寫數(shù)據(jù),訪問內(nèi)存較少,速度較快,理論上來說只需要完全踢出fun1的數(shù)據(jù)1次即可。
五、局部變量VS靜態(tài)變量
很多人認(rèn)為局部變量在使用到時才會在內(nèi)存中分配儲存單元,而靜態(tài)變量在程序的一開始便存在于內(nèi)存中,所以使用靜態(tài)變量的效率應(yīng)該比局部變量高,其實(shí)這是一個誤區(qū),使用局部變量的效率比使用靜態(tài)變量要高。
這是因?yàn)榫植孔兞渴谴嬖谟诙褩V械?,對其空間的分配僅僅是修改一次esp寄存器的內(nèi)容即可(即使定義一組局部變量也是修改一次)。而局部變量存在于堆棧中最大的好處是,函數(shù)能重復(fù)使用內(nèi)存,當(dāng)一個函數(shù)調(diào)用完畢時,退出程序堆棧,內(nèi)存空間被回收,當(dāng)新的函數(shù)被調(diào)用時,局部變量又可以重新使用相同的地址。當(dāng)一塊數(shù)據(jù)被反復(fù)讀寫,其數(shù)據(jù)會留在CPU的一級緩存(Cache)中,訪問速度非??臁6o態(tài)變量卻不存在于堆棧中。
可以說靜態(tài)變量是低效的。
六、避免使用多重繼承
在C++中,支持多繼承,即一個子類可以有多個父類。書上都會跟我們說,多重繼承的復(fù)雜性和使用的困難,并告誡我們不要輕易使用多重繼承。其實(shí)多重繼承并不僅僅使程序和代碼變得更加復(fù)雜,還會影響程序的運(yùn)行效率。
這是因?yàn)樵贑++中每個對象都有一個this指針指向?qū)ο蟊旧?,而C++中類對成員變量的使用是通過this的地址加偏移量來計(jì)算的,而在多重繼承的情況下,這個計(jì)算會變量更加復(fù)雜,從而降低程序的運(yùn)行效率。而為了解決二義性,而使用虛基類的多重繼承對效率的影響更為嚴(yán)重,因?yàn)槠淅^承關(guān)系更加復(fù)雜和成員變量所屬的父類關(guān)系更加復(fù)雜。
七、盡量少使用dynamic_cast
dynamic_cast的作用是進(jìn)行指針或引用的類型轉(zhuǎn)換,dynamic_cast的轉(zhuǎn)換需要目標(biāo)類型和源對象有一定的關(guān)系:繼承關(guān)系。 實(shí)現(xiàn)從子類到基類的指針轉(zhuǎn)換,實(shí)際上這種轉(zhuǎn)換是非常低效的,對程序的性能影響也比較大,不可大量使用,而且繼承關(guān)系越復(fù)雜,層次越深,其轉(zhuǎn)換時間開銷越大。在程序中應(yīng)該盡量減少使用。
八、減少除法運(yùn)算的使用
無論是整數(shù)還是浮點(diǎn)數(shù)運(yùn)算,除法都是一件運(yùn)算速度很慢的指令,在計(jì)算機(jī)中實(shí)現(xiàn)除法是比較復(fù)雜的。所以要減少除法運(yùn)算的次數(shù),下面介紹一些簡單方法來提高效率:
1、通過數(shù)學(xué)的方法,把除法變?yōu)槌朔ㄟ\(yùn)算,如if(a > b/c),如果a、b、c都是正數(shù),則可寫成if(a*c > b)
2、讓編譯器有優(yōu)化的余地,如里你要做的運(yùn)算是int型的n/8的話,寫成(unsigned)n/8有利于編譯器的優(yōu)化。而要讓編譯器有優(yōu)化的余地,則除數(shù)必須為常數(shù),而這也可以用const修飾一個變量來達(dá)到目的。
九、將小粒度函數(shù)聲明為內(nèi)聯(lián)函數(shù)(inline)
正如我們所知,調(diào)用函數(shù)是需要保護(hù)現(xiàn)場,為局部變量分配內(nèi)存,函數(shù)結(jié)束后還要恢復(fù)現(xiàn)場等開銷,而內(nèi)聯(lián)函數(shù)則是把它的代碼直接寫到調(diào)用函數(shù)處,所以不需要這些開銷,但會使程序的源代碼長度變大。
所以若是小粒度的函數(shù),如下面的Max函數(shù),由于不需要調(diào)用普通函數(shù)的開銷,所以可以提高程序的效率。
int Max(int a, int b) { return a>b?a:b; }
十、多用直接初始化
與直接初始化對應(yīng)的是復(fù)制初始化,什么是直接初始化?什么又是復(fù)制初始化?舉個簡單的例子,
ClassTest ct1; ClassTest ct2(ct1); //直接初始化 ClassTest ct3 = ct1; //復(fù)制初始化
那么直接初始化與復(fù)制初始化又有什么不同呢?直接初始化是直接以一個對象來構(gòu)造另一個對象,如用ct1來構(gòu)造ct2,復(fù)制初始化是先構(gòu)造一個對象,再把另一個對象值復(fù)制給這個對象,如先構(gòu)造一個對象ct3,再把ct1中的成員變量的值復(fù)制給ct3,從這里,可以看出直接初始化的效率更高一點(diǎn),而且使用直接初始化還是一個好處,就是對于不能進(jìn)行復(fù)制操作的對象,如流對象,是不能使用賦值初始化的,只能進(jìn)行直接初始化。可能我說得不太清楚,那么下面就引用一下經(jīng)典吧!
以下是Primer是的原話:
“當(dāng)用于類類型對象時,初始化的復(fù)制形式和直接形式有所不同:直接初始化直接調(diào)用與實(shí)參匹配的構(gòu)造函數(shù),復(fù)制初始化總是調(diào)用復(fù)制構(gòu)造函數(shù)。復(fù)制初始化首先使用指定構(gòu)造函數(shù)創(chuàng)建一個臨時對象,然后用復(fù)制構(gòu)造函數(shù)將那個臨時對象復(fù)制到正在創(chuàng)建的對象”,還有一段這樣說,“通常直接初始化和復(fù)制初始化僅在低級別優(yōu)化上存在差異,然而,對于不支持復(fù)制的類型,或者使用非explicit構(gòu)造函數(shù)的時候,它們有本質(zhì)區(qū)別:
ifstream file1("filename")://ok:direct initialization
ifstream file2 = "filename";//error:copy constructor is private
”
注:如還對直接初始化和復(fù)制初始化有疑問,可以參考一下前面的一篇文章:
C++直接初始化與復(fù)制初始化的區(qū)別深入解析,里面有有關(guān)直接初始化和復(fù)制初始化的詳細(xì)解釋。
補(bǔ)充:
這里只是一點(diǎn)點(diǎn)的建議,雖然說了這么多,但是還是要說一下的就是:要避免不必要的優(yōu)化,避免不成熟的優(yōu)化,不成熟的優(yōu)化的是錯誤的來源,因?yàn)榫幾g器會為你做很多你所不知道的優(yōu)化。
希望本文所述對提高大家C++程序設(shè)計(jì)效率能有所幫助。
相關(guān)文章
c++函數(shù)指針和回調(diào)函數(shù)示例
這篇文章主要介紹了c++函數(shù)指針和回調(diào)函數(shù)示例,需要的朋友可以參考下2014-05-05C語言SetConsoleCursorInfo函數(shù)使用方法
這篇文章介紹了C語言SetConsoleCursorInfo函數(shù)的使用方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12C++程序中使用Windows系統(tǒng)Native Wifi API的基本教程
這篇文章主要介紹了C++程序中使用Windows系統(tǒng)Native Wifi API的基本教程,包括在程序中控制無線網(wǎng)卡開關(guān)的方法,需要的朋友可以參考下2016-03-03C++實(shí)操之內(nèi)聯(lián)成員函數(shù)介紹
大家好,本篇文章主要講的是C++實(shí)操之內(nèi)聯(lián)成員函數(shù)介紹,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12OpenCV圖像分割中的分水嶺算法原理與應(yīng)用詳解
這篇文章主要為大家詳細(xì)介紹了OpenCV圖像分割中的分水嶺算法原理與應(yīng)用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-01-01