事實上,效率可不是一個開玩笑的事情。一個太大或太慢的程序它們的優點無論多麼引人注目都不會為人們所接受。本來就應該這樣。軟件是用來幫助我們更好地工作,說運行速度慢才是更好的,說需要32MB內存的程序比僅僅需要16MB內存的程序好,說占用100MB磁盤空間的程序比僅僅占用50MB磁盤空間的程序好,這簡直是無稽之談。而且盡管有一些程序確是為了進行更復雜的運算才占用更多的時間和空間,但是對於許多程序來說只能歸咎於其糟糕的設計和馬虎的編程。
在用C++寫出高效地程序之前,必須認識到C++本身絕對與你所遇到的任何性能上的問題無關。如果想寫出一個高效的C++程序,你必須首先能寫出一個高效的程序。太多的開發人員都忽視了這個簡單的道理。是的,循環能夠被手工展開,移位操作(shift operation)能夠替換乘法,但是如果你所使用的高層算法其內在效率很低,這些微調就不會有任何作用。當線性算法可用時你是否還用二次方程式算法?你是否一遍又一遍地計算重復的數值?如果是的話,可以毫不誇張地把你的程序比喻成一個二流的觀光勝地,即如果你有額外的時間,才值得去看一看。
本章的內容從兩個角度闡述效率的問題。第一是從語言獨立的角度,關注那些你能在任何語言裡都能使用的東西。C++為它們提供了特別吸引人的實現途徑,因為它對封裝的支持非常好,從而能夠用更好的算法與數據結構來替代低效的類實現,同時接口可以保持不變。
第二是關注C++語言本身。高性能的算法與數據結構雖然非常好,但如果實際編程中代碼實現得很粗糙,效率也會降低得相當多。潛在危害性最大的錯誤是既容易犯又不容易察覺的錯誤,瀕繁地構造和釋放大量的對象就是一種這樣的錯誤。過多的對象構造和對象釋放對於你的程序性能來說就象是在大出血,在每次建立和釋放不需要的對象的過程中,寶貴的時間就這麼流走了。這個問題在C++程序中很普遍,我將用四個條款來說明這些對象從哪裡來的,在不影響程序代碼正確性的基礎上如何消除它們。
建立大量的對象不會使程序變大而只會使其運行速度變慢。還有其它一些影響性能提高的因素,包括程序庫的選擇和語言特性的實現(implementations of language features)。在下面的條款中我也將涉及。
在學習了本章內容以後,你將熟悉能夠提高程序性能的幾個原則,這些原則可以適用於你所寫的任何程序。你將知道如何准確地防止在你的軟件裡出現不需要的對象,並且對編譯器生成可執行代碼的行為有著敏銳的感覺。
俗話說有備無患(forewarned is forearmed)。所以把下面的內容想成是戰斗前的准備。
牢記80-20准則(80-20 rule)
80-20准則說的是大約20%的代碼使用了80%的程序資源;大約20%的代碼耗用了大約80%的運行時間;大約20%的代碼使用了80%的內存;大約20%的代碼執行80%的磁盤訪問;80%的維護投入於大約20%的代碼上;通過無數台機器、操作系統和應用程序上的實驗這條准則已經被再三地驗證過。80-20准則不只是一條好記的慣用語,它更是一條有關系統性能的指導方針,它有著廣泛的適用性和堅實的實驗基礎。
當想到80-20准則時,不要在具體數字上糾纏不清,一些人喜歡更嚴格的90-10准則,而且也有一些試驗證據支持它。不管准確地數字是多少,基本的觀點是一樣的:軟件整體的性能取決於代碼組成中的一小部分。
當程序員力爭最大化提升軟件的性能時,80-20准則既簡化了你的工作又使你的工作變得復雜。一方面80-20准則表示大多數時間你能夠編寫性能一般的代碼,因為80%的時間裡這些代碼的效率不會影響到整個系統的性能,這會減少一些你的工作壓力。而另一方面這條准則也表示如果你的軟件出現了性能問題,你將面臨一個困難的工作,因為你不僅必須找到導致問題的那一小塊代碼的位置,還必須尋找方法提高它們的性能。這些任務中最困難的一般是找到系統瓶頸。基本上有兩個不同的方法用來尋找:大多數人用的方法和正確的方法。
大多數人尋找瓶頸的方法就是猜。通過經驗、直覺、算命紙牌、顯靈板、傳聞或者其它更荒唐的東西,一個又一個程序員一本正經地宣稱程序的性能問題已被找到,因為網絡的延遲,不正確的內存分配,編譯器沒有進行足夠的優化或者一些笨蛋主管拒絕在關鍵的循環裡使用匯編語句。這些評估總是以一種帶有嘲笑的盛氣凌人的架式發布出來,通常這些嘲笑者和他們的預言都是錯誤的。
大多數程序員在他們程序性能特征上的直覺都是錯誤的,因為程序性能特征往往不能靠直覺來確定。結果為提高程序各部分的效率而傾注了大量的精力,但是對程序的整體行為沒有顯著的影響。例如在程序裡加入能夠最小化計算量的奇特算法和數據結構,但是如果程序的性能限制主要在I/O上(I/O-bound)那麼就絲毫起不到作用。采用I/O性能強勁的程序庫代替編譯器本身附加的程序庫(參見條款23),如果程序的性能瓶頸主要在CPU上(CPU-bound),這種方法也不會起什麼作用。
在這種情況下,面對運行速度緩慢或占用過多內存的程序,你該如何做呢?80-20准則的含義是胡亂地提高一部分程序的效率不可能有很大幫助。程序性能特征往往不能靠直覺確定,這個事實意味著試圖猜出性能瓶頸不可能比胡亂地提高一部分程序的效率這種方法好到哪裡去。那麼會後什麼結果呢?
結果是用經驗識別程序20%的部分只會導致你心痛。正確的方法是用profiler程序識別出令人討厭的程序的20%部分。不是所有的工作都讓profiler去做。你想讓它去直接地測量你感興趣的資源。例如如果程序太緩慢,你想讓profiler告訴你程序的各個部分都耗費了多少時間。然後你關注那些局部效率能夠被極大提高的地方,這也將會很大地提高整體的效率。
profiler告訴你每條語句執行了多少次或各函數被調用了多少次,這是一個作用有限的工具。從提高性能的觀點來看,你不用關心一條語句或一個函數被調用了多少次。畢竟很少遇到用戶或程序庫的調用者抱怨執行了太多的語句或調用了太多的函數。如果軟件足夠快,沒有人關心有多少語句被執行,如果程序運行過慢,不會有人關心語句有多麼的少。他們所關心的是他們厭惡等待,如果你的程序讓他們等待,他們也會厭惡你。
不過知道語句執行或函數調用的頻繁程度,有時能幫助你洞察軟件內部的行為。例如如果你建立了100個某種類型的對象,會發現你調用該類的構造函數有上千次,這個信息無疑是有價值的。而且語句和函數的調用次數能間接地幫助你理解不能直接測量的軟件行為。例如如果你不能直接測量動態內存的使用,知道內存分配函數和內存釋函數的調用頻率也是有幫助的。(也就是,operators new, new[], delete, and delete[]—參見條款8)
當然即使最好的profiler也是受其處理的數據所影響。如果用缺乏代表性的數據profile你的程序,你就不能抱怨profiler會導致你優化程序的那80%的部分,從而不會對程序通常的性能有什麼影響。記住profiler僅能夠告訴你在某一次運行(或某幾次運行)時一個程序運行情況,所以如果你用不具有代表性的輸入數據profile一個程序,那你所進行的profile也沒有代表型。相反這樣做很可能導致你去優化不常用的軟件行為,而在軟件的常用領域,則對軟件整體的效率起相反作用(即效率下降)。
防止這種不正確的結果,最好的方法是用盡可能多的數據profile你的軟件。此外,你必須確保每組數據在客戶(或至少是最重要的客戶)如何使用軟件的方面能有代表性。通常獲取有代表性的數據是很容易的,因為許多客戶都願意讓你用他們的數據進行profile。畢竟你是為了他們需求而優化軟件。