陳皓:程式碼執行的效率

發表於2012-07-14

來源:陳皓

在《效能調優攻略》裡,我說過,要調優性需要找到程式中的Hotspot,也就是被呼叫最多的地方,這種地方,只要你能優化一點點,你的效能就會有質的提高。在這裡我給大家舉三個關於程式碼執行效率的例子(它們都來自於網上)

第一個例子

PHP中Getter和Setter的效率來源reddit

這個例子比較簡單,你可以跳過。

考慮下面的PHP程式碼:我們可看到,使用Getter/Setter的方式,效能要比直接讀寫成員變數要差一倍以上。

這個並沒有什麼稀,因為有函式呼叫的開銷,函式呼叫需要壓棧出棧,需要傳值,有時還要需要中斷,要乾的事太多了。所以,程式碼多了,效率自然就慢了。所有的語言都這個德行,這就是為什麼C++要引入inline的原因。而且Java在開啟優化的時候也可以優化之。但是對於動態語言來說,這個事就變得有點困難了。

你可能會以為使用下面的程式碼(Magic Function)會好一些,但實際其效能更差。

動態語言的效率從來都是一個問題,如果你需要PHP有更好的效能,你可能需要使用FaceBook的HipHop來把PHP編譯成C語言。

第二個例子

為什麼Python程式在函式內執行得更快?來源StackOverflow

考慮下面的程式碼,一個在函式體內,一個是全域性的程式碼。

函式內的程式碼執行效率為 1.8s

函式體外的程式碼執行效率為 4.5s

不用太糾結時間,只是一個示例,我們可以看到效率查得很多。為什麼會這樣呢?我們使用 dis module 反彙編函式體內的bytecode 程式碼,使用 compile builtin 反彙編全域性bytecode,我們可以看到下面的反彙編(注意我高亮的地方)

陳皓:程式碼執行的效率

我們可以看到,差別就是 STORE_FAST 和 STORE_NAME,前者比後者快很多。所以,在全域性程式碼中,變數i成了一個全域性變數,而函式中的i是放在本地變數表中,所以在全域性變數表中查詢變數就慢很多。如果你在main函式中宣告global i 那麼效率也就下來了。原因是,本地變數是存在一個陣列中(直到),用一個整型常量去訪問,而全域性變數存在一個dictionary中,查詢很慢。

(注:在C/C++中,這個不是一個問題)

第三個例子

為什麼排好序的資料在遍歷時會更快?來源StackOverflow

參看如下C/C++的程式碼:

陳皓:程式碼執行的效率

如果你的data陣列是排好序的,那麼效能是1.93s,如果沒有排序,效能為11.54秒。差5倍多。無論是C/C++/Java,或是別的什麼語言都基本上一樣。

這個問題的原因是—— branch prediction (分支預判)偉大的stackoverflow給了一個非常不錯的解釋。

考慮我們一個鐵路分叉,當我們的列車來的時候, 扳道員知道分個分叉通往哪,但不知道這個列車要去哪兒,司機知道要去哪,但是不知道走哪條分叉。所以,我們需要讓列車停下來,然後司機和扳道員溝通一下。這樣的效能太差了。

所以,我們可以優化一下,那就是猜,我們至少有50%的概率猜對,如果猜對了,火車行駛效能巨高,猜錯了,就得讓火車退回來。如果我猜對的概率高,那麼,我們的效能就會高,否則老是猜錯了,效能就很差。

陳皓:程式碼執行的效率

Image by Mecanismo, from Wikimedia Commons:http://commons.wikimedia.org/wiki/File:Entroncamento_do_Transpraia.JPG

我們的if-else 就像這個鐵路分叉一樣,下面紅箭頭所指的就是搬道器。

陳皓:程式碼執行的效率

那麼,我們的搬道器是怎麼預判的呢?就是使用過去的歷史資料,如果歷史資料有90%以上的走左邊,那麼就走左邊。所以,我們排好序的資料就更容易猜得對。

排好序的

從上面我們可以看到,排好序的資料更容易預測分支。

未排序的

對此,那我們怎麼辦?我們需要在這種迴圈中除去if-else語句。比如:

我們把條件語句:

變成:

“沒有分叉”的效能基本上和“排好序有分支”一個樣,無論是C/C++,還是Java。

注:在GCC下,如果你使用 -O3 or -ftree-vectorize 編譯引數,GCC會幫你優化分叉語句為無分叉語句。VC++2010沒有這個功能。

最後,推薦大家一個網站——Google Speed,網站上的有一些教程告訴你如何寫出更快的Web程式

(全文完)

 

相關文章