對於每個程序員來講,程序的運行效率都是一個值得重視,併爲之付出努力的問題。可是程序性能的優化也是一門複雜的學問,須要不少的知識,然而並非每一個程序員都具有這樣的知識,並且論述如何優化程序提升程序運行效率的書籍也不多。可是這並不等於咱們能夠忽略程序的運行效率,下面就介紹一下本人積累的一些簡單實用的提升程序運行效率的方法,但願對你們有所幫助。程序員
注:以C/C++程序爲例數組
1、儘可能減小值傳遞,多用引用來傳遞參數。 至於其中的緣由,相信你們也很清楚,若是參數是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修飾符,引用是不能引用常量的。優化
2、++i和i++引伸出的效率問題 看了上面的第一點,你可能以爲,那不就是多調用了四個函數而已,你可能對此不屑一顧。那麼來看看下面的例子,應該會讓你大吃一驚。this
至於整型變量的前加和後加的區別相信你們也是很清楚的。然而在這裏我想跟你們談的倒是C++類的運算符重載,爲了與整形變量的用法一致,在C++中重載運算符++時通常都會把前加和後加都重載。你可能會說,你在代碼中不會重載++運算符,可是你敢說你沒有使用過類的++運算符重載嗎?迭代器類你總使用過吧!可能到如今你還不是很懂我在說什麼,那麼就先看看下面的例子吧,是本人爲鏈表寫的一個內部迭代器。設計
_SingleList::Iterator& _SingleList::Iterator::operator++()//前加 { pNote = pNote->pNext; return *this; } _SingleList::Iterator _SingleList::Iterator::operator++(int)//後加 { Iterator tmp(*this); pNote = pNote->pNext; return tmp; } 從後加的實現方式能夠知道,對象利用本身建立一個臨時對象(本身在函數調用的一個複製),而後改變本身的狀態,並返回這個臨時對象,而前加的實現方式時,直接改變本身的內部狀態,並返回本身的引用。指針
從第一點的論述能夠知道後加實現時會調用複製構造函數,在函數返回時還要調用析構函數,而因爲前加實現方式直接改變對象的內部狀態,並返回本身的引用,至始至終也沒有建立新的對象,因此也就不會調用構造函數和析構函數。對象
然而更加糟糕的是,迭代器一般是用來遍歷容器的,它大多應用在循環中,試想你的鏈表有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 }
若是你的習慣很差,寫了第二種形式,那麼很不幸,作一樣的事情,就是由於一個前加和一個後加的區別,你就要調用多200個函數,其對效率的影響可就不可忽視了。
3、循環引起的討論1(循環內定義,仍是循環外定義對象) 請看下面的兩段代碼: 代碼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 }
你會以爲哪段代碼的運行效率較高呢?代碼1科學家是代碼2?其實這種狀況下,哪段代碼的效率更高是不肯定的,或者說是由這個類ClassTest本向決定的,分析以下:
對於代碼1:須要調用ClassTest的構造函數1次,賦值操做函數(operator=)100次;對於代碼2:須要高用(複製)構造函數100次,析構函數100次。
若是調用賦值操做函數的開銷比調用構造函數和析構函數的總開銷小,則第一種效率高,不然第二種的效率高。
4、循環引起的討論2(避免過大的循環) 如今請看下面的兩段代碼, 代碼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()是沒有關聯的,即兩段代碼所產生的結果是同樣的。
以代碼的層面上來看,彷佛是代碼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次便可。
5、局部變量VS靜態變量 不少人認爲局部變量在使用到時纔會在內存中分配儲存單元,而靜態變量在程序的一開始便存在於內存中,因此使用靜態變量的效率應該比局部變量高,其實這是一個誤區,使用局部變量的效率比使用靜態變量要高。
這是由於局部變量是存在於堆棧中的,對其空間的分配僅僅是修改一次esp寄存器的內容便可(即便定義一組局部變量也是修改一次)。而局部變量存在於堆棧中最大的好處是,函數能重複使用內存,當一個函數調用完畢時,退出程序堆棧,內存空間被回收,當新的函數被調用時,局部變量又能夠從新使用相同的地址。當一塊數據被反覆讀寫,其數據會留在CPU的一級緩存(Cache)中,訪問速度很是快。而靜態變量卻不存在於堆棧中。
能夠說靜態變量是低效的。
6、避免使用多重繼承 在C++中,支持多繼承,即一個子類能夠有多個父類。書上都會跟咱們說,多重繼承的複雜性和使用的困難,並告誡咱們不要輕易使用多重繼承。其實多重繼承並不只僅使程序和代碼變得更加複雜,還會影響程序的運行效率。
這是由於在C++中每一個對象都有一個this指針指向對象自己,而C++中類對成員變量的使用是經過this的地址加偏移量來計算的,而在多重繼承的狀況下,這個計算會變量更加複雜,從而下降程序的運行效率。而爲了解決二義性,而使用虛基類的多重繼承對效率的影響更爲嚴重,由於其繼承關係更加複雜和成員變量所屬的父類關係更加複雜。
7、儘可能少使用dynamic_cast dynamic_cast的做用是進行指針或引用的類型轉換,dynamic_cast的轉換須要目標類型和源對象有必定的關係:繼承關係。 實現從子類到基類的指針轉換,實際上這種轉換是很是低效的,對程序的性能影響也比較大,不可大量使用,並且繼承關係越複雜,層次越深,其轉換時間開銷越大。在程序中應該儘可能減小使用。
8、減小除法運算的使用 不管是整數仍是浮點數運算,除法都是一件運算速度很慢的指令,在計算機中實現除法是比較複雜的。因此要減小除法運算的次數,下面介紹一些簡單方法來提升效率: 一、經過數學的方法,把除法變爲乘法運算,如if(a > b/c),若是a、b、c都是正數,則可寫成if(a*c > b) 二、讓編譯器有優化的餘地,如裏你要作的運算是int型的n/8的話,寫成(unsigned)n/8有利於編譯器的優化。而要讓編譯器有優化的餘地,則除數必須爲常數,而這也能夠用const修飾一個變量來達到目的。
9、將小粒度函數聲明爲內聯函數(inline) 正如咱們所知,調用函數是須要保護現場,爲局部變量分配內存,函數結束後還要恢復現場等開銷,而內聯函數則是把它的代碼直接寫到調用函數處,因此不須要這些開銷,但會使程序的源代碼長度變大。
因此如果小粒度的函數,以下面的Max函數,因爲不須要調用普通函數的開銷,因此能夠提升程序的效率。 int Max(int a, int b) { return a>b?a:b; }
10、多用直接初始化 與直接初始化對應的是複製初始化,什麼是直接初始化?什麼又是複製初始化?舉個簡單的例子, 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++的一大誤區——深刻解釋直接初始化與複製初始化的區別,裏面有有關直接初始化和複製初始化的詳細解釋。
補充: 這裏只是一點點的建議,雖說了這麼多,可是仍是要說一下的就是:要避免沒必要要的優化,避免不成熟的優化,不成熟的優化的是錯誤的來源,由於編譯器會爲你作不少你所不知道的優化。
之後還發現有簡單的提升運行效率的方法,還會繼續補充......