第55條:謹慎地進行優化

weixin_34413065發表於2017-08-09

有三條與優化有關的格言是每個人都應該知道的。這些格言我們可能已經耳熟能詳,但是,如果對它們還不太熟悉,請看下面:
  很多計算上的過失都被歸咎於效率(沒有必要達到的效率),而不是任何其他的原因,——甚至包括盲目地做傻事。
                      ——William A.Wulf[Wulf72]
  不要去計校效率上的一些小小的得失,在97%的情況下。不成熟的優化才是一切問題的根源。
                      ——Donald E.Knuth[Knuth74]
  在優化方面,我們應該遵守兩條規則:
  規則1:不要進行優化。
  規則2(僅針對專加):還是不要進行優化一一也就是說,在你還沒有絕對清晰的未優化方案之前,請不要進行優化.
                      ——M.A.Jackson[Jackson75]
  所有這些格言都比Java程式設計語言的出現早了2 0年。它們講述了一個關於優化的深刻真理:優化的弊大於利,特別是不成熟的優化。在優化過程中,產生的軟體可能既不快速,也不正確,而且還不容易修正。
  不要因為效能而犧牲合理的結構。要努力編寫好的程式而不足快的欄序。如果好的程式不夠快,它的結構將使它可以得到優化。好的程式體現了資訊隱藏 (information hiding)的原則:只要有可能,它們就會把設計決策集中在單個模組中,因此,可以改變單個決策,而不會影響到系統的其他部分(見第13條:使類和成員的可訪問性最小化)。
  這並不意味著在完成程式之前就可以忽略效能問題。實現上的問題可以通過後期的優化而得到修正,但是,遍佈全域性並且限制效能的結構缺陷幾乎是不可能被改正的.除非重新編寫系統。在系統完成之後再改變設計的某個基本方面,會導致系統的結構很不好,從而難以維護和改進。因此,必須在設計過程中考慮到效能間題。
  努力避免那些限制效能的設計決策。當一個系統設計完成之後,其中最難以更改的元件是那些指定了模組之間互動關係以及模組與外界互動關係的元件。在這些設計元件之中,最主要的是API線路層(wire-Ievel)協議以及永久資料格式。這些設計元件不僅在事後難以甚至不可能改變,而且它們都有可能對系統本該達到的效能產生嚴重的限制。
  要考慮API設計決策的效能後果。使公有的型別成為可變的(mutable )。這可能會導致大量不必要的保護性拷貝(見第39條:必要時進行保護性拷貝 )。同樣地,在適合使用複合模式的公有類中使用繼承,會把這個類與它的超類永遠地束縛在一起,從而人為地限制了子類的效能(見第16條:複合優先於繼承)。最後一個例子,在API中使用實現型別而不是介面,會把你束縛在一個具休的實現上,即使將來出現更快的實現你也無法使用(見第52條:通過介面引用物件)。
  API設計對於效能的影響是非常實際的。考慮java.awt.Component類中的getSize方法。這個決定就是,這個注重效能的方法將返回Dimension例項,與此密切相關的決定是,Dimension例項是可變的,迫使這個方法的任何實現都必須為每個呼叫分配一個新的Dimension例項。儘管在現代V M上分配小物件的開銷並不大,但是分配數百萬個不必要的物件仍然會嚴重地損害效能。

    public Dimension getSize() {
        return size();
    }
    @Deprecated
    public Dimension size() {
        return new Dimension(width, height);
    }

在這種情況下,有幾種可供選擇的替換方案。理想情況下,Dimension應該是不可變的(見第15條:使可變性最小化);另一種方案是,用兩個方法來替換getSize方法,它們分別返回Dimension物件的單個基本元件。實際上.在1.2發行版本中,出於效能方面的原因,兩個這樣的方法已經被加入到Component API中。然而,原先的客戶端程式碼仍然可以使用getSize方法,但是仍然要承受原始API設計決策所帶來的效能影響。
  幸運的是,一般而言,好的API設計也會帶來好的效能。為獲得好的效能而對API進行包裝,這是一種非常不好的想法。導致你對API進行包裝的效能因素可能會在平臺未來的發行版本中,或者在將來的底層軟體中不復存在,但是披包裝的API以及由它引起的問題將永遠困擾著你。
  一旦謹慎地設計了程式,井且產生了一個清晰、簡明、結構良好的實現,那麼就到了該考慮優化的時候了,假定此時你對於程式的效能還不滿意。
  回想一下Jackson的兩條優化規則:“不要優化"以及“(僅針對專家)還是不要優化"。他可以再增加一條:在每次試圖做優化之前和之後,要對效能進行測量。你可能會驚訝於自己的發現。試圖做的優化通常對於效能井沒有明顯的影響,有時候甚至會使效能變得更差。主要的原因在於,要猜出程式把時間花在哪些地方並不容易。你認為程式慢的地方可能井沒有問題,這種情況下實際上是在浪費時間去嘗試優化。大多數人認為:程式把8 0%的時間花在2 0%的程式碼上了。
  效能剖析工具有助於你決定應該把優化的重心放在哪裡。這樣的工具可以為你提供執行時的資訊,比如每個方法大致上花費了多少時間、它被呼叫多少次。除了確定優化的重點之外,它還可以警告你是否需要改變演算法。如果一個平方級(或更差)的演算法潛藏在程式中,無論怎麼調整和優化都很難解決問題。你必須用更有效的演算法來替換原來的演算法。系統中的程式碼越多,使用效能剖析器就顯得越發重要。這就好像要在一堆乾草中尋找一根針:達堆乾草越大,使用金屬探測器就越有用。JDK帶了簡單的效能剖析器,現代的IDE也提供了更加成熟的效能剖折工具。
  在Java乎臺上對優化的結果進行測量,比在其他的傳統平臺上更有必要,因為Java程式設計語言沒有很強的效能模型 。各種基本操作的相對開銷也沒有明確定義。程式設計師所編寫的程式碼與CPU執行的程式碼之間存在“語義溝(semantic gap)”,而且這條語義溝比傳統編譯語言中的更大,這使得要想可靠地預測出任何優化的效能結果都非常困難。大量流傳的關於效能的說法最終都被證明為半真半假。或者根本就不正確。
  不僅Java的效能模型未得到很好的定義。而且在不同的JVM實現.,或者不同的發行版本,以及不同的處理器,在它們這些當中也都各不相同。如果將要在多個JVM實現和多種硬體平臺上執行程式,很重要的一點是,需要在每個Java實現上測量優化效果。有時候,還必須在從不同JVM實現或者硬體平臺上得到的效能結果之中進行權衡。
  總而言之,不要費力去編寫快速的程式——應該努力編寫好的程式,速度自然會隨之而來。在設計系統的時候,特別是在設計API、線路層協議和永久資料格式的時候,一定要考慮效能的因素。當構建完系統之後,要測量它的效能。如果它足夠快,你的任務就完成了。如果不夠快,則可以在效能剖析器的幫助下,找到問題的根源,然後設法優化系統中相關的部分。第一個步驟是檢查所選擇的演算法:再多的低層優化也無法彌補演算法的選擇不當。必要時重複這個過程,在每次改變之後都要測量效能,直到滿意為止。

相關文章