天天看點

More Exceptional C++中文版試讀(優化與性能)

[Herb Sutter 的名作 More Exceptional C++ 中文版即将出版。作為本書譯者,我很高興将本書推薦給大家。征得華中科技大學出版社同意,我将公開部分譯稿,敬請大家批評指正。 ] 

優化與性能

對程式員來說,效率總是很重要。在C和C++的傳統中,效率是重要支柱之一,“不要為沒有使用的東西支付任何成本”這一指導原則——也稱為零成本原則——總是語言設計和程式庫設計的中心,而且,在很大程度上也确實得到了實作。

在本章以及書後的兩個附錄中,我們對一些重要的C++優化問題進行了深入的觀察,并分析了它們對現實世界代碼的影響。應該在何時優化你的代碼?如何優化?inline到底做了些什麼?為什麼花哨的優化能夠(而且确實會)讓我們陷入麻煩?最後一點,并且對我來說最有趣的一點:如果你是在寫多線程代碼,上面一些問題的答案會如何發生變化?畢竟,我們關心的是現實世界中的效率問題;雖然C++标準對線程避而不談,但在程式設計領域的第一線,每天都有越來越多的程式員在寫多線程的C++代碼。他們會關心這些問題的答案。

條款12:内聯 難度:4
和大多數人的看法相反,關鍵字inline其實并不是什麼魔法。事實上,隻是在被恰當運用的時候,它才會成為有用的工具。問題是:什麼時候該使用它呢?

1. inline有什麼作用?

2. 将函數内聯會提高效率嗎?

3. 何時應當決定使用内聯函數?如何決定?

解答
1. inline 有什麼作用?

将一個函數聲明為inline意味着告訴編譯器:編譯器可以将這個函數代碼的拷貝直接放在每一個使用這個函數的地方。編譯器可以選擇這麼做,或不這麼做;如果編譯器确實這麼做了,将會避免函數調用的發生。

2. 将函數内聯會提高效率嗎?

不一定。

首先,在回答這個問題之前,如果不先問問自己到底想優化“什麼”,你就會落入一個著名的陷阱。第一個問題應該是:“你所說的效率指的是什麼?”在上面的提問中,所謂的效率指的是程式體積嗎?抑或是記憶體占用?執行時間?開發速度?編譯時間?或是其它什麼東西?

其次,和大多數人的看法相反:對于效率的各個方面,内聯可能使之改善,也可能使之惡化:

a) 程式體積。許多程式員認為,内聯一定會增加程式的體積,因為程式擁有的将不隻是函數代碼的一份拷貝,編譯器會在使用了那個函數的每個地方生成一份拷貝。通常來說這是對的,但并非總是如此。如果和“編譯器為了執行函數調用而不得不生成的代碼”的體積相比,内聯函數的體積比它還小,那麼,内聯會減小程式體積。

b) 記憶體占用。除了(上面所說的)基本程式體積外,内聯通常對程式的記憶體使用沒有影響,或極少有影響。

c) 執行時間。很多程式員認為,将函數内聯一定會提高運作速度,因為它避免了函數調用的開銷;而且,透過了函數調用這層“障礙”,編譯器的優化程式就有了更多大顯身手的機會。這可能是正确的,但不總是正确:如果函數不是被極其頻繁地調用,整個程式的執行時間通常不會有明顯的改善。實際上,事情有可能适得其反。如果内聯增加了調用函數(calling function)的體積,它會降低調用者的“引用局部性(locality of reference)”(譯注:參見[Meyers96]條款18);這意味着,如果調用者的内部指令循環(inner loop)不再和處理器高速緩存的大小相比對,整個程式的執行速度實際上會降低。

不要忘記:客觀地說,大多數程式的速度并非受限于CPU。最常見的瓶頸可能在于I/O上的限制,這包括很多方面,如網絡帶寬或延遲、對檔案或資料庫的通路,等等。

d) 開發速度和編譯時間。為了得到最有效的利用,被内聯的代碼必須對調用者可見;這意味着,調用者必須依賴于被内聯代碼的内部細節。依賴另一個子產品的内部實作細節必然增加子產品的實際耦合性(但不會增加理論耦合性,因為調用者實際上沒有使用被調用者的任何内部實作。)通常情況下,當普通函數被修改時,調用者無需重新編譯,隻需重新連結。當内聯函數被修改時,調用者必須重新編譯。還有,内聯函數本身會在調試時期單獨影響開發速度,因為,要想單步跟蹤到内聯函數的内部,或者在内聯函數内部管理斷點,對大多數調試器來說會更困難。

有一種情況下,一些人會認為内聯是對開發速度的一種優化(這一點有一些争議)——為了避免讓資料成員為公有成員,提供一個存取函數(accessor,見後)是好的做

法,但寫這樣一個存取函數的代價可能很高。這種情況下,一些人會認為,使用内聯會帶來好的編碼風格和更好的子產品獨立性。

最後請記住,如果你想用什麼方式提高效率,總是先借助你的算法和資料結構。它們會給你的程式帶來數量級的整體改善,而内聯之類的過程優化(process optimization)通常(注意,“通常”)收效甚微。

不妨說“現在不行” 3. 何時應當決定使用内聯函數?如何決定?

和使用其它任何一種優化技術一樣,答案是:在分析工具告訴你這樣做之前,不要貿然行事。這條原則有幾個合理的例外——在有些場合下你可以毫不遲疑地内聯一個函數:例如空函數,而且會持續保持為空;或者,你非得這麼做不可的時候——例如,在寫一個非輸出模闆(non-exported template)的時候。

設計準則

使用優化的第一條原則:不要使用它。

使用優化的第二條原則:還是不要使用它。

結論:隻要增加了耦合性,内聯就總是會帶來成本;絕對不要為某個東西事先支付成本,除非你知道它會帶來好處——也就是說,回報大于支出。

“但我總能找到瓶頸在哪兒!”你會這樣想。别着急,不是你一個人這樣想。大多數程式員都或多或少地這樣想過,但他們還是錯了。完全錯了。對于代碼中真正的瓶頸所在,程式員是臭名昭著的猜測者。有時侯,我們撞大運似地蒙對了。但大多數時候,我們的猜測是錯誤的。

通常,隻有實驗資料(或稱分析結果)可以幫助我們找出真正的熱點所在。如果不借助某種分析工具,十有八九,一個程式員不可能識别出他(她)的代碼中頭号熱點或瓶頸所在。幹這行很多年了,我曾遇到過幾個反對這一事實的程式員,他們(或他們的同僚)堅持認為這一事實對他們不适用,聲稱自己一向能夠“感覺到”自己代碼中的熱點所在。多年來,我從沒有看到過這樣的宣言變成一貫的事實。我們善于欺騙自己。

最後注意,使用這條準則的另一個實際原因是:對于哪一個内聯函數不應該被内聯,分析工具并不善于識别。

關于密集計算任務(例如數值計算程式庫)

一些人要編寫短小緊湊的程式庫代碼,例如進階科學和工程計算程式庫,這些人有時可以憑直覺使用内聯,并做得很不錯。然而,即使是這些程式員,他們也傾向于明智地使用内聯,認為優化宜遲不宜早。注意,寫一個子產品,然後通過“打開内聯”和“關閉内聯”的方式來比較性能,這通常不是一個好主意,因為“全開”和“全閉”是一種很粗糙的分析方法,它隻能告訴你一般情況。它不會告訴你哪個函數受益,也不會告訴你每個函數受益多少。即使在這些情況下,你也應該使用分析工具并基于它的建議去優化。

關于存取函數

會有一些人争辯說:隻有一行代碼的存取函數(例如“X& Y::f() { return myX_; }”)是個合理的例外,它“可以”或“應該” 被自動内聯。我知道這樣做的道理,但要小心行事。不管怎麼說,所有被内聯的代碼都會增加耦合性。是以,除非你事先确信内聯會帶來好處,否則,将使用内聯的決定延遲到分析之後是沒有壞處的。到了那時,如果分析工具真的指出内聯會有好處,你至少知道,你正在做的是值得做的事;而且,你也将耦合和可能的編譯開銷延遲了——延遲到你确實知道内聯真的有必要的時候。這樣做不會讓你吃虧的,真的!

設計準則
在性能分析證明确實必要之前,避免内聯或詳細優化。

繼續閱讀