天天看點

兩個最容易被人忽略的基本代碼優化技術

       Dr. Dobb’s Blogger的Walter Bright曾寫了一篇博文《Overlooked Essentials For Optimizing Code》,為我們總結了兩個最容易被人忽略的基本代碼優化技術。酷殼個人網站版主陳皓對本文進行了翻譯,現轉載于此,供大家學習。全文如下:

       我編寫程式至今有35年了,我做了很多關于程式執行速度方面優化的工(一個示例),我也看過其它人做的優化。我發現有兩個最基本的優化技術總是被人所忽略。

注意,這兩個技術并不是避免時機不成熟的優化。并不是把冒泡排序變成快速排序(算法優化)。也不是語言或是編譯器的優化。也不是把 i*4寫成i<<2 的優化。

        這兩個技術是:

        使用 一個profiler。

        檢視程式執行時的彙編碼。

        使用這兩個技術的人将會成功地寫出運作快的代碼,不會使用這兩個技術的人則不行。下面讓我為你細細道來。

使用一個 Profiler

        我們知道,程式運作時的90%的時間是用在了10%的代碼上。我發現這并不準确。一次又一次地,我發現,幾乎所有的程式會在1%的代碼上花了99%的運作時間。但是,是哪個1%?一個好的Profiler可以告訴你這個答案。就算我們需要使用100個小時在這1%的代碼上進行優化,也比使用100個小時在其它99%的代碼上優化産生的效益要高得多得多。

       問題是什麼?人們不用profiler?不是。我工作過的一個地方使用了一個華麗而奢侈的Profiler,但是自從購買這個Profiler後,它的包裝3年來還是那麼的暫新。為什麼人們不用?我真的不知道。有一次,我和我的同僚去了一個負載過大的交易所,我同僚堅持說他知道哪裡是瓶頸,畢竟,他是一個很有經驗的專家。最終,我把我的Profiler在他的項目上運作了一下,我們發現那個瓶頸完全在一個意想不到的地方。

就像是賽車一樣。團隊是赢在傳感器和日志上,這些東西提供了所有的一切。你可以調整一下賽車手的褲子以讓其在比賽過程中更舒服,但是這不會讓你赢得比賽,也不會讓你更有競争力。如果你不知道你的速度上不去是因為引擎、排氣裝置、空體動力學、輪胎氣壓,或是賽車手,那麼你将無法獲勝。程式設計為什麼會不同呢?隻要沒有測量,你就永遠無法進步。

        這個世界上有太多可以使用的Profiler了。随便找一個你就可以看到你的函數的調用層次,調用的次數,以前每條代碼的時間分解表(甚至可以到彙編級)。我看過太多的程式員回避使用Profiler,而是把時間花在那些無用的,錯誤的方向上的“優化”,而被其競争對手所羞辱。(譯者陳皓注:使用Profiler時,重點需要關注:1)花時間多的函數以優化其算法,2)調用次數巨多的函數——如果一個函數每秒被調用300K次,你隻需要優化出0.001毫秒,那也是相當大的優化。這就是作者所謂的1%的代碼占用了99%的CPU時間)

 檢視彙編代碼

        幾年前,我有一個同僚,Mary Bailey,她在華盛頓大學教矯正代數(remedial algebra),有一次,她在黑闆上寫下:

        x + 3 = 5

       然後問他的學生“求解x”,然後學生們不知道答案。于是她寫下:

       __ + 3 = 5

          然後,再問學生“填空”,所有的學生都可以回答了。未知數x就像是一個有魔法的字母讓大家都在想“x意味着代數,而我沒有學過代數,是以我就不知道這個怎麼做”。

彙程式設計式就是程式設計世界的代數。如果某人問我“inline函數是否被編譯器展開了?”或是問我“如果我寫下i*4,編譯器會把其優化為左移位操作嗎?”。這個時候,我都會建議他們看看編譯器的彙編碼。這樣的回答是不是很粗暴和無用?通常,在我這樣回答了提問者後,提問都通常都會說,對不起,我不知道什麼是彙編!甚至C++的專家都會這麼回答。

       彙編語言是最簡單的程式設計語言了(就算是和C++相比也是這樣的),如:

ADD ESI,x

就是(C風格的代碼)

ESI += x;

而:

CALL foo

則是:

foo();

        細節因為CPU的種類而不同,但這就是其如何工作的。有時候,我們甚至都不需要細節,隻需要看看彙編碼的長啥樣,然後和源代碼比一比,你就可以知道彙編代碼很多很多了。

那麼,這又如何幫助代碼優化?舉個例子,我幾年前認識一個程式員認為他應該去發現一個新的更快的算法。他有一個benchmark來證明這個算法,并且其寫了一篇非常漂亮的文章關于他的這個算法。但是,有人看了一下其原來算法以及新算法的彙編,發現了他的改進版本的算法允許其編譯器把兩個除法操作變成了一個。這和算法真的沒有什麼關系。我們知道除法操作是一個很昂貴的操作,并且在其算法中,這倆個除法操作還在一個内嵌循環中,是以,他的改進版的算法當然要快一些。但,隻需要在原來的算法上做一點點小的改動——使用一個除法操作,那麼其原來的算法将會和新的一樣快。而他的新發現什麼也不是。

下一個例子,一個D使用者張貼了一個 benchmark 來顯示 dmd (Digital Mars D 編譯器)在整型算法上的很糟糕,而ldc (LLVM D 編譯器) 就好很多了。對于這樣的結果,其相當的有意見。我迅速地看了一下彙編,發現兩個編譯器編譯出來相當的一緻,并沒有什麼明顯的東西要對2:1這麼大的不同而負責。但是我們看到有一個對long型整數的除法,這個除法調用了運作庫。而這個庫成為消耗時間的殺手,其它所有的加減法都沒有速度上的影響。出乎意料地,benchmark 和算法代碼生成一點關系也沒有,完全就是long型整數的除法的問題。這暴露了在dmd的運作庫中的long型除法的實作很差。修正後就可以提高速度。是以,這和編譯器沒有什麼關系,但是如果不看彙編,你将無法發現這一切。

檢視彙編代碼經常會給你一些意想不到的東西讓你知道為什麼程式的性能是那樣。一些意想不到的函數調用,預料不到的自傲,以及不應該存在的東西,等等其實所有的一切。但也不需要成為一個彙編代碼的黑客才能幹的事。

 結論

        如果你覺得需要程式有更好的執行速度,那麼,最基本的方法就是使用一個profiler和願意去檢視一下其彙編代碼以找到程式的瓶頸。隻有找到了程式的瓶頸,此時才是真正在思考如何去改進的時候,比如思考一個更好的算法,使用更快的語言優化,等等。

正常的做法是制勝法寶是挑選一個最佳的算法而不是進行微優化。雖然這種做法是無可異議的,但是有兩件事情是學校沒有教給你而需要你重點注意的。第一個也是最重要的,如果你優化的算法沒沒有參與到你程式性能中的算法,那麼你優化他隻是在浪費時間和精力,并且還轉移了你的注意力讓你錯過了應該要去優化的部分。第二點,算法的性能總和處理的資料密切相關的,就算是冒泡排序有那麼多的笑柄,但是如果其處理的資料基本是排好序的,隻有其中幾個資料是未排序的,那麼冒泡排序也是所有排序算法裡性能最好的。是以,擔心沒有使用好的算法而不去測量,隻會浪費時間,無論是你的還是計算機的。

        就好像賽車零件的訂購速底是不會讓你更靠進冠軍(就算是你正确安裝零件也不會),沒有Profiler,你不會知道問題在哪裡,不去看彙編,你可能知道問題所在,但你往往不知道為什麼。

原文連結:Overlooked Essentials For Optimizing Code

譯文連結:http://coolshell.cn/articles/2967.html

繼續閱讀