最近開發的一個程序,對代碼的速度要求很高,同時由於已實現的代碼速度不能滿足要求,因此進行了搜索。收藏此篇。
文章來源:http://www.kuqin.com/language/20090314/39898.html
摘要
不管是否願意承認,每個人都希望程序的運行速度越快越好。每天人們都你追我趕,好像明天就是末日。而同時,公關部的那些家伙則不停的吼叫著,說他們的新引擎比其他人的更“快”更“好”。
我並不打算告訴你如何讓你的代碼跑得比別人的快。我只是想告訴你,如何讓你的代碼更快、更高效,當然,是跟你原來的代碼相比。
我講述的內容主要涉及三個概念,這三者之間的關系相當復雜:
1、代碼執行時間
2、代碼/程序大小
3、程序設計本身的開支
我始終堅信應該保持這三者之間的平衡,尤其在某些情況下,2、3兩項直接影響了代碼的執行時間。
在本文中,我將講述一些可能有助於你提高代碼執行效率的方法。我會從最簡單的優化方法開始,然後逐漸深入到那些比較復雜的技術。現在我們首先從一個不太顯眼的地方開始:編譯器。
考慮到讀者中有一些經驗豐富的程序員,我的敘述會盡可能簡單,以避免因為細節太多而顯得雜亂不堪。
第一節 公欲善其事,必先利其器
這一節的內容似乎不說也罷,不過仔細想想,你對你手中的編譯器到底了解多少?你知道它可以為哪些處理器生成代碼嗎?你知道它可以進行哪些類型的優化嗎?你知道它的語言不兼容性嗎?
當你想要寫出點什麼的時候,尤其是當你希望你的代碼運行如飛的時候,了解這些內容將是至關重要的。
舉例來說,最近在GameDev的討論組裡有人問關於Microsoft Visual C++的“Release Mode”的問題。這是一個標准編譯器選項,如果你使用特定的編譯器,你就應該知道它的意思。如果你不知道,那很遺憾,你並不真正會使用你花費了大量的金錢買來的東西。簡單來說,“Release Mode”會刪除所有debug用的代碼,進行所有可能的編譯代碼優化,生成更小的可執行文件,還讓這個文件運行的更快。它可能還會有一些其它的功能,如果你感興趣,請閱讀編譯器的相關文檔。
看到了吧,如果你以前並不知道這個“Release Mode”,我現在就可以告訴你一個讓你的代碼運行更快的方法,而且這個方法不需要你修改任何代碼!
目標平台也是非常重要的。現在,你遇到的最低檔的可能就是Intel Pentium處理器了,不過如果你使用10年前的編譯器,那麼它不會做任何針對Pentium的優化。去找一個最新的編譯器,它可能會大大提高程序的運行速度,同樣,也不需要你對代碼做任何的修改。
另外還要注意一些事:你的編譯器有沒有代碼分析(profiling)工具?如果你連這個都不知道,那麼你就不要指望編寫出更快的代碼了。如果你還不知道什麼是代碼分析工具,那麼你還需要更多的學習。一個代碼分析工具就是一個用來獲得程序的運行時間的東東。你在代碼分析器(profiler)中運行你的程序,做一些操作,然後再從你的程序中退出,就可以獲得一個關於每個函數耗時的報告。你可以根據這個報告找到代碼的運行瓶頸——就是你的代碼中花費時間最多的部分。對這些部分作一些特定的優化比隨隨便便的在每個地方都做一點優化效果要好多了。
不要說“但是我知道我的瓶頸在哪!”它們可不是光用腦子就可以找到的,尤其是在使用第三方API和程序庫時。幾個星期前我還遇到一個類似的問題,在一個視頻程序裡,顯示每一幀時都會莫名其妙的產生狀態切換,而這個動作占用了總執行時間的25%。通過簡單的添加一條測試語句(測試狀態是否已經被設置),我把相應的那個函數從分析得到的50個最昂貴的函數列表中剔除了。
看上去在大多數情況下,使用分析器可以很容易達到目的,但事實上並非如此。你必須找到程序中的關鍵路徑。所謂關鍵路徑就是程序大部分運行時間都在執行的路徑。對關鍵路徑進行優化可以顯著的提高運行效率,你的用戶也會因此而高興。
另一種情況是,也許你發現在某個函數中,時間開支最大的步驟是裝載一個特定的文件,但是你知道這種情況只會在應用程序啟動時發生一次。對這個函數進行優化也許可以讓程序的總運行時間減少幾秒鐘,但不會提升正常使用時的效率。事實上,這表明你沒有進行足夠的代碼分析,因為在正常使用時,這個函數所占用的時間百分比將會越來越低,而你的關鍵路徑所占用的時間百分比將會一路飙升。
我想以上這些內容能夠使你對這些工具有了一些了解。
代碼分析工具實在是太好了,記得一定要用!
如果你還沒有代碼分析器,你可以試試Intel的VTune profiler。你可以免費試用它一個月。在下面這個網址下載它http://developer.intel.com/vtune/analyzer/。
在本文的下一部分,我將告訴你如何讓你的C/C++編譯器做你想讓它做的事。
第二節 Inlining,inline關鍵字
什麼是inlining?我會通過描述inline關鍵字來回答這個問題。
Inline關鍵字告訴編譯器“在適當的地方展開函數”,它工作起來很像是C和C++中的宏(#define),但是有一點不同。Inline函數是類型安全的,其主要作用是幫助編譯器進行代碼優化。有了它,你就可以同時具有宏的速度(沒有函數調用的額外開銷)和函數的類型安全性,以及一大堆其它好處。
還有什麼好處呢?大多數編譯器在同一時間內只能優化一個模塊中的代碼。通常就是一個.h/.cpp文件對。使用inline函數,就使得編譯器對在不同的模塊中的函數也可以進行代碼優化,比如消除返回值拷貝,消除多余的臨時變量,等等。如果你想要了解更多關於編譯器優化的內容,請參考本文結尾處給出的參考文獻,尤其是那本講述C++高效編程的書。
可怕的inline關鍵字。我不得不這樣說,因為關於它的誤解實在太多了。Inline關鍵字並不強迫編譯器inline特定的函數,而只是建議編譯器這樣做。以下內容引自MSDN:
“The inline keyword tells the compiler that inline expansion is preferred. However, the compiler can create a separate instance of the function (instantiate) and create standard calling linkages instead of inserting the code inline.”(inline關鍵字告訴編譯器最好進行inline擴展。但是,編譯器可能會創建一個獨立的函數實例和一個標准的調用連接,而不是將代碼內聯的插入。)
某些情況下編譯器會忽略你的inline請求,這些情況包括:在inline函數中使用了循環;在inline函數中調用其它inline函數;遞歸。
上面引用的那段話還隱含著其它一些內容:一個聲明為inline的函數,必須進行內部連接。這就是說,如果你的inline函數在另一個object文件中實現,你的連接器在連接這個函數時就會卡殼。ANSI標准倒是提供了一種方法解決這個問題,可惜的是目前為止Visual C++(6.0)尚不支持這種解決辦法。
“那麼,”你要問了,“到底應該怎麼辦呢?”答案很簡單:總是在同一個模塊中實現inline函數。這個方法做起來很簡單,只要將整個函數實現寫到.h文件中,並且在所有用到這個函數的模塊中包含這個.h文件。也許這並不想你想象中的那麼美好,不過它的確可以正常工作。
事實上,考慮到隱藏實現的問題(我是個面向對象偏執狂),我並不喜歡這個方法。但是最近我的確使用這個方法編寫了很多類。有一個好處是,我不需要輸入inline這個關鍵字——如果你把整個函數定義放進類定義中,編譯器會自動的把它看成inline函數。如果一個類的所有函數都應該是inline的,那麼我就把整個類定義及實現都寫進頭文件中。我建議你只在真正迫切的需要提高運行速度時才這樣做,當然,你也不在意太多的人share你的代碼。
第三節 搭乘類高速列車
設計執行速度快的類是C++程序設計的關鍵。我用一個3d向量類來說明這個問題(這在我的工作中是很常見的類)。事實上,就在前幾個星期,我剛剛完成了一個向量類。在編寫這個類的一個月裡,我犯下了太多錯誤。
一個向量類是必須的,因為工作中有大量的向量數學運算,顯然每次都要反復書寫相同的內容。如果你想提高編碼效率,同時又不想犧牲代碼運行速度,那麼就要編寫一個向量類,我的這一個叫作CVector3f(3f的意思是三個float數據)。為了提高代碼的可讀性和可維護性,我希望利用C++偉大的特性之一——運算符重載(operator overloading)實現一些運算符函數(+,-,*)。
在最初的設計中,我很快的實現了一個構造函數、一個拷貝構造函數、一個析構函數以及上面提到的那三個運算符。設計過程中,我沒有特別考慮效率的問題,也沒有使用inline函數,只是簡單的把函數聲明放入頭文件,把函數實現放入.cpp文件中。
下一步是讓它跑得更快。我做的第一件事是在頭文件中將所有成員函數聲明為inline函數。如果編譯器真的將它們處理成inline函數,那麼我們就可以節省下函數調用的額外開銷。對於我的向量類中的那些小函數來說,執行速度有了顯著的提升,不過對於那些較大的函數來說,這樣做可能不會有明顯的效果。
我想到的第二件事是:我們真的需要析構函數嗎?正常情況下編譯器會為我們生成一個空的析構函數,通常它會比我們寫的析構函數效率更高。在我們的向量類中,並沒有什麼東西需要析構,那麼為什麼還要浪費時間?
運算符也可以跑得更快。先前的運算符函數大致如下:
CVector3f operator+( CVector3f v )
{
CVector3f returnVector;
returnVector.m_x = m_x + v.m_x;
returnVector.m_y = m_y + v.m_y;
returnVector.m_z = m_z + v.m_z;
return returnVector;
}
這段代碼隱藏著眾多的多余代碼,著實令人煩惱。我們來仔細看看這段代碼,代碼的第一行聲明並構造了一個臨時變量。這就是說,這個對象的默認構造函數被調