提高java程序性能設計

 翻譯 by SuperMMX
  閱讀整個的 "爲性能而設計" 系列:
  第一部分: 接口事宜
  第二部分: 減少對象創建
  第三部分: 遠程接口 (March 23, 2001)
  第一部分: 接口事宜
  概要
  許多通常的 Java 性能問題都起源於在設計過程早期中的類設計的思想, 早在許多開發者開始考慮性能問題之前. 在這個系列中, Brian Goetz討論了通常的 Java性能上的冒險以及怎麼在設計時候避免它們.

  許多程序員在開發週期的後期纔可是考慮性能管理. 他們常常把性能優化拖延到最後, 希望能完全避免 -- 有時候這種策略是成功的. 但是早期的設計思想可以影響性能優化的需求及其成功. 如果性能是你的程序的一個重要指標, 那麼性能管理應該從第一天起就和設開發週期整合在一起.
  這個系列探索一些早期的設計思想能夠極大影響應用程序性能的方法. 在這篇文章中, 我專注於最通常的性能問題中的一個: 臨時變量的創建. 一個類的對象創建方式常常在設計時候就確定了的 -- 但不是故意的 --, 就爲後來的性能問題種下了種子.
  性能問題有各種形式. 最容易調整的是那些你簡單地爲計算選擇了一個錯誤的算法 -- 就象使用使用冒泡算法來對一個大數據集進行排序, 或者在使用一個經常使用的數據項時不是做緩衝, 而是每次都計算. 你可以使用概要分析來簡單地找出這些瓶頸, 一旦找到了,你可以很容易地改正. 但是, 許多 Java 性能問題來自一個更深的, 更難改正的源頭 -- 一個程序組件的接口設計.
  今天大多數程序是由內部開發的或者外部買來的組件構建而成. 甚至在程序不是很大地依於已經存在的組件時, 面向對象的設計過程也鼓勵應用程序包裝成組件, 這樣就簡化了設計, 開發和測試過程. 這些優勢是不可否認的, 你應該認識到這些組件實現的接口可能極大地影響使用它們的程序的行爲和性能.
  在這一點上, 你可能要問什麼樣的接口和性能相關. 一個類的接口不僅定義了這個類可以實現那些功能, 也可以定義它的對象創建行爲和使用它的方法調用序列. 一個類怎樣定義它的構造函數和方法決定了一個對象是否可以重用, 它的方法是否要創建 -- 或者要求它的客戶端創建 -- 中間對象,  以及一個客戶端需要調用多少方法來使用這個類.這些因素都會影響程序的性能.
  注意對象的創建
  一個最基本的 Java 性能管理原則就是: 避免大量的對象創建. 這不是說你應該不創建任何對象而放棄面向對象的好處. 但是你必須在執行性能相關的代碼時, 在緊循環中注意對象的創建. 對象的創建是如此地高代價, 以至於你應該在要求性能的情況下避免不必要的臨時或者中間對象的創建.
  String 類是在那些處理文本的程序中對象創建的主要來源. 因爲 String 是不可修改的,每當一個 String 修改或創建, 就必須創建一個新的對象. 結果就是, 關注性能的程序應該避免大量 String 的使用. 但是, 這通常是不可能的. 甚至當你從你的代碼中完全除去對 String 的依賴, 你常常會發現你自己在使用一些具有根據 String 定義的接口的組件.所以, 你最後不得不使用 String.
  例子: 正規表達式匹配
  作爲一個例子, 假設你寫一個叫做 MailBot 的郵件服務器. MailBot 需要處理 MIME 頭格式 -- 象發送日期或者發送者的 email 地址 -- 在每個信息的頂部. 使用一個匹配正規表達式的組件來使處理 MIME 頭的過程簡單一些. MailBot 足夠聰明, 不爲每個頭的行或者頭的元素創建一個 String 對象. 相反, 它用輸入的文本填充了一個字符緩衝區, 通過對緩衝區的索引來確定要處理的頭的位置. MailBot 會調用正規表達式匹配器來處理每個頭行, 所以匹配器的性能就非常重要. 我們以一個正規表達式匹配器類的拙劣的接口作爲例子:
public class AwfulRegExpMatcher { 
  /** Create a matcher with the given regular expression and which will
      operate on the given input string */
  public AwfulRegExpMatcher(String regExp, String inputText);
  /** Retrieve the next match of the pattern against the input text,
      returning the matched text if possible or null if not */
  public String getNextMatch();
}
  甚至在這個類實現了一個有效的正規表達式匹配的算法的時候, 任何大量使用它的程序仍然難以忍受. 既然匹配器對象和輸入的文本聯繫起來, 每一次你調用它, 你必須創建一個新的匹配器對象. 既然你的目標是減少不必要的對象的創建, 那麼使這個匹配器可以賾將會是一個明顯的開始.
  下面的類定義演示了你的匹配器的另一個可能的接口, 允許你重用這個匹配器, 但仍然很壞.
public class BadRegExpMatcher { 
  public BadRegExpMatcher(String regExp);
  /** Attempts to match the specified regular expression against the input
      text, returning the matched text if possible or null if not */
  public String match(String inputText);
  /** Get the next match against the input text, or return null if no match */
  public String getNextMatch();
}
  忽略正規表達式匹配中的精細點 -- 象返回匹配的子表達式, 這個看起來無害的類定義會出什麼問題呢? 從功能上來看, 沒有. 但是從性能的角度來看, 許多. 首先, 匹配器需要它的調用者創建一個 String 來代表要匹配的文本. MailBot 試圖避免創建 String對象, 但是當它要找到一個要做正規表達式解析的頭時, 它不得不創建一個 String 來滿足 BadRegExpMatcher:
BadRegExpMatcher dateMatcher = new BadRegExpMatcher(...);
while (...) {
  ...
  String headerLine = new String(myBuffer, thisHeaderStart, 
  thisHeaderEnd-thisHeaderStart);
  String result = dateMatcher.match(headerLine);
  if (result == null) { ... }
}
  第二, 匹配器創建了結果字符串甚至當 MailBot 只關心是否匹配了, 不需要匹配的文本時,這意味着要簡單使用 BadRegExpMatcher 來確認一個日期頭是否匹配一個特定的格式, 你必須創建兩個 String 對象 -- 匹配器的輸入和匹配的結果. 兩個對象可能看起來不多,但是如果你給 MailBot 處理的每個郵件的每個頭行都創建兩個對象, 這會極大地影響性能. 錯誤不在於 MailBot 的設計, 而在於 BadRegExpMatcher 類的設計 -- 或者使用.
  注意返回一個輕量型的 Match 對象 -- 可以提供 getOffset(), getLength(), egetMatchString() 方法 -- 而不是返回一個 String, 這不會很大提高性能. 因爲創建一個 Match 對象可能比創建一個 String 代價要小 -- 包括產生一個 char[] 數組和複製數據, 你仍然創建了一箇中間對象, 對你的調用者來說沒有價值.
  這已經足夠壞了, BadREgExpMatcher 強迫你使用它想看到的輸入形式, 而不是你可以提供的更有效的形式. 但是使用 BadRegExpMathcer 還有另一個危險, 潛在地給 MailBot的性能帶來更大的冒險: 在處理郵件頭的時候, 你開始有避免使用 String 的傾向. 但是既然你被迫創建許多 String 對象來滿足 BadRegExpMatcher, 你可能被引誘而放棄這個目標, 更加自由地使用 String. 現在, 一個組件的糟糕的設計已經影響了使用它的程序.
甚至你後來找到了一個更好的正規表達式的組件, 不需要你提供一個 String, 那時你的整個程序都會受影響.
  一個好一些的接口
  你怎樣定義 BadRegExpMatcher, 而不引起這樣的問題呢? 首先, BadRegExpMatcher 應該不規定它的輸入. 它應該可以接受它的調用者能夠有效提供的各種輸入格式. 第二, 它不應該自動給匹配結果產生一個 String; 應該返回足夠的信息, 這樣調用者如果願意的話可以生成它. (爲方便着想, 它可以提供一個方法來做這件事, 但不是必須的) 這裏有一個好一些的接口:
class BetterRegExpMatcher { 
  public BetterRegExpMatcher(...);
  /** Provide matchers for multiple formats of input -- String,
      character array, and subset of character array.  Return -1 if no
      match was made; return offset of match start if a match was
      made.  */
  public int match(String inputText);
  public int match(char[] inputText);
  public int match(char[] inputText, int offset, int length);
  /** Get the next match against the input text, if any */
  public int getNextMatch();
  /** If a match was made, returns the length of the match; between
      the offset and the length, the caller should be able to
      reconstruct the match text from the offset and length */
  public int getMatchLength();
  /** Convenience routine to get the match string, in the event the
      caller happens to wants a String */
  public String getMatchText();
}
  新的接口減少了調用者把輸入轉換成匹配器希望的格式這個要求. MailBot 現在可以象下面這樣調用 match():
int resultOffset = dateMatcher.match(myBuffer, thisHeaderStart, 
thisHeaderEnd-thisHeaderStart);
if (resultOffset < 0) { ... }
  這就解決了不創建任何新對象的目標. 作爲一個附加的獎勵, 它的接口設計風格加到了Java 的 "lots-of-simgle-methos" 設計哲學中.
  額外的對象創建給性能的確切的衝擊依賴於 matth() 所作的工作量. 你可以通過創建和計時兩個正規表達式匹配器類, 來確定一個性能差別的上限. 在 Sun JDK 1.3 中, 上面的代碼片段在 BetterRegExpMatcher 類中大約比 BadRegExpMatcher 類要快 50 倍左右. 使用一個簡單的字串匹配的實現, BetterRegExpMatcher 比相對應的 BadRegExpMatcher 要快5倍。
  交換類型
  BadRegExpMatcher 強迫 MailBot 把輸入文本從字符數組轉換成 String, 結果是造成了一些不必要的對象的創建. 更具諷刺意味的是, BadRegExpMatcher 的許多實現都立即把 String 轉換成一個字符數組, 使它容易對輸入文本進行訪問. 這樣不僅僅申請了另一齠象, 並且還意味着你做完了所有的工作, 最後的形式和開始時一樣. MailBot 和 BadRegExpMatcher都不想處理 String -- String 只是看起來象是在組件之間傳遞文本的很明顯的格式.
  在上面的 BadRegExpMatcher 例子中, String 類是作爲一個交換類型的. 一個交換類型是一種不管是調用者還是被調用者都不想使用或者以它作爲數據格式的一種類型, 但是兩個都能很容易地轉換它或者從它轉換. 以交換類型定義接口在保持靈活性的同時減少了接口的複雜性, 但是有時簡單性導致了高代價的性能.
  一個交換類型最典型的例子是 JDBC ResultSet 接口. 它不可能象任何本地數據庫提供的數據集一樣提供它的 ResultSet 接口, 但是 JDBC 驅動通過實現一個 ResultSet 可以很容易地把數據庫提供的本地數據表示包裝起來. 同樣, 客戶端程序也不能象這樣表示數據記錄, 但是你幾乎可以沒有困難地把 ResultSet 轉換爲想要的數據表示. 在 JDBC 的例子中,你接受了這個層次的花費, 因爲它帶來了標準化和跨數據庫實現的可移植性的好處. 但是,要注意交換類型帶來的性能代價.
  這完全不值得, 使用交換類型對性能的衝擊不容易度量. 如果你對上面調用 BadRegExpMatcher的代碼片段做測試的話, 它會在運行時創建 MailBot 的輸入 String; 但是, String 的產生只用來滿足 BadRegExpMatcher. 如果你想評定一個組件對程序性能的真正的衝擊, 你應該不僅僅度量它的代碼的資源使用狀況, 還有那些使用它和恢復的代碼. 這對於標準的測試工具此很難完成.
  結論
  不是所有的程序都關注於性能的, 不是所有的程序都有性能問題. 但是對那些關注這些的程序, 這篇文章所提到的都很重要, 因爲它們不是在最後一分鐘就可以修改的. 既然在你編寫寫代碼使用一個類以後再修改它的接口非常困難, 那麼在你的設計時期就花費一點額外的時間來考慮性能特性.
  在第二部分, 我會演示一些利用可修改性和不可修改性來減少不必要的對象創建的方法.
About the author
Brian Goetz is a professional software developer with over 15 years of experience. He is a principal consultant at Quiotix, a software development and consulting firm located in Los Altos, Calif.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章