# 附錄D 性能
“本附錄由Joe Sharp投稿,并獲得他的同意在這兒轉載。請聯系`SharpJoe@aol.com`”
Java語言特別強調準確性,但可靠的行為要以性能作為代價。這一特點反映在自動收集垃圾、嚴格的運行期檢查、完整的字節碼檢查以及保守的運行期同步等等方面。對一個解釋型的虛擬機來說,由于目前有大量平臺可供挑選,所以進一步阻礙了性能的發揮。
“先做完它,再逐步完善。幸好需要改進的地方通常不會太多。”(Steve McConnell的《About performance》[16])
本附錄的宗旨就是指導大家尋找和優化“需要完善的那一部分”。
## D.1 基本方法
只有正確和完整地檢測了程序后,再可著手解決性能方面的問題:
(1) 在現實環境中檢測程序的性能。若符合要求,則目標達到。若不符合,則轉到下一步。
(2) 尋找最致命的性能瓶頸。這也許要求一定的技巧,但所有努力都不會白費。如簡單地猜測瓶頸所在,并試圖進行優化,那么可能是白花時間。
(3) 運用本附錄介紹的提速技術,然后返回步驟1。
為使努力不至白費,瓶頸的定位是至關重要的一環。Donald Knuth[9]曾改進過一個程序,那個程序把50%的時間都花在約4%的代碼量上。在僅一個工作小時里,他修改了幾行代碼,使程序的執行速度倍增。此時,若將時間繼續投入到剩余代碼的修改上,那么只會得不償失。Knuth在編程界有一句名言:“過早的優化是萬惡之源”(Premature optimization is the root of all evil)。最明智的做法是抑制過早優化的沖動,因為那樣做可能遺漏多種有用的編程技術,造成代碼更難理解和操控,并需更大的精力進行維護。
## D.2 尋找瓶頸
為找出最影響程序性能的瓶頸,可采取下述幾種方法:
### D.2.1 安插自己的測試代碼
插入下述“顯式”計時代碼,對程序進行評測:
```
long start = System.currentTimeMillis();
// 要計時的運算代碼放在這兒
long time = System.currentTimeMillis() - start;
```
利用`System.out.println()`,讓一種不常用到的方法將累積時間打印到控制臺窗口。由于一旦出錯,編譯器會將其忽略,所以可用一個“靜態最終布爾值”(`Static final boolean`)打開或關閉計時,使代碼能放心留在最終發行的程序里,這樣任何時候都可以拿來應急。盡管還可以選用更復雜的評測手段,但若僅僅為了量度一個特定任務的執行時間,這無疑是最簡便的方法。
`System.currentTimeMillis()`返回的時間以千分之一秒(1毫秒)為單位。然而,有些系統的時間精度低于1毫秒(如Windows PC),所以需要重復`n`次,再將總時間除以`n`,獲得準確的時間。
### D.2.2 JDK性能評測[2]
JDK配套提供了一個內建的評測程序,能跟蹤花在每個例程上的時間,并將評測結果寫入一個文件。不幸的是,JDK評測器并不穩定。它在JDK 1.1.1中能正常工作,但在后續版本中卻非常不穩定。
為運行評測程序,請在調用Java解釋器的未優化版本時加上`-prof`選項。例如:
```
java_g -prof myClass
```
或加上一個程序片(Applet):
```
java_g -prof sun.applet.AppletViewer applet.html
```
理解評測程序的輸出信息并不容易。事實上,在JDK 1.0中,它居然將方法名稱截短為30字符。所以可能無法區分出某些方法。然而,若您用的平臺確實能支持`-prof`選項,那么可試試Vladimir Bulatov的“HyperPorf”[3]或者Greg White的“ProfileViewer”來解釋一下結果。
### D.2.3 特殊工具
如果想隨時跟上性能優化工具的潮流,最好的方法就是作一些Web站點的常客。比如由Jonathan Hardwick制作的“Tools for Optimizing Java”(Java優化工具)網站:
http://www.cs.cmu.edu/~jch/java/tools.html
### D.2.4 性能評測的技巧
+ 由于評測時要用到系統時鐘,所以當時不要運行其他任何進程或應用程序,以免影響測試結果。
+ 如對自己的程序進行了修改,并試圖(至少在開發平臺上)改善它的性能,那么在修改前后應分別測試一下代碼的執行時間。
+ 盡量在完全一致的環境中進行每一次時間測試。
+ 如果可能,應設計一個不依賴任何用戶輸入的測試,避免用戶的不同反應導致結果出現誤差。
## D.3 提速方法
現在,關鍵的性能瓶頸應已隔離出來。接下來,可對其應用兩種類型的優化:常規手段以及依賴Java語言。
### D.3.1 常規手段
通常,一個有效的提速方法是用更現實的方式重新定義程序。例如,在《Programming Pearls》(編程珠璣)一書中[14],Bentley利用了一段小說數據描寫,它可以生成速度非常快、而且非常精簡的拼寫檢查器,從而介紹了Doug McIlroy對英語語言的表述。除此以外,與其他方法相比,更好的算法也許能帶來更大的性能提升——特別是在數據集的尺寸越來越大的時候。欲了解這些常規手段的詳情,請參考本附錄末尾的“一般書籍”清單。
### D.3.2 依賴語言的方法
為進行客觀的分析,最好明確掌握各種運算的執行時間。這樣一來,得到的結果可獨立于當前使用的計算機——通過除以花在本地賦值上的時間,最后得到的就是“標準時間”。
| 運算 | 示例 | 標準時間 |
| --- | --- | --- |
| 本地賦值 | `i=n;` | 1.0 |
| 實例賦值 | `this.i=n;` | 1.2 |
| `int`自增 | `i++;` | 1.5 |
| `byte`自增 | `b++;` | 2.0 |
| `short`自增 | `s++;` | 2.0 |
| `float`自增 | `f++;` | 2.0 |
| `double`自增 | `d++;` | 2.0 |
| 空循環 | `while(true) n++;` | 2.0 |
| 三元表達式 | `(x<0) ?-x : x` | 2.2 |
| 算術調用 | `Math.abs(x);` | 2.5 |
| 數組賦值 | a[0] = n; | 2.7 |
| `long`自增 | l++; | 3.5 |
| 方法調用 | `funct();` | 5.9 |
| `throw`或`catch`異常 | `try{ throw e; }`或`catch(e){}` | 320 |
| 同步方法調用 | `synchMehod();` | 570 |
| 新建對象 | `new Object();` | 980 |
| 新建數組 | `new int[10];` | 3100 |
通過自己的系統(如我的Pentium 200 Pro,Netscape 3及JDK 1.1.5),這些相對時間向大家揭示出:新建對象和數組會造成最沉重的開銷,同步會造成比較沉重的開銷,而一次不同步的方法調用會造成適度的開銷。參考資源[5]和[6]為大家總結了測量用程序片的Web地址,可到自己的機器上運行它們。
(1) 常規修改
下面是加快Java程序關鍵部分執行速度的一些常規操作建議(注意對比修改前后的測試結果)。
| 將... | 修改成... | 理由 |
| --- | --- | --- |
| 接口 | 抽象類(只需一個父時) | 接口的多個繼承會妨礙性能的優化 |
| 非本地或數組循環變量 | 本地循環變量 |根據前表的耗時比較,一次實例整數賦值的時間是本地整數賦值時間的1.2倍,但數組賦值的時間是本地整數賦值的2.7倍 |
| 鏈接列表(固定尺寸) | 保存丟棄的鏈接項目,或將列表替換成一個循環數組(大致知道尺寸) | 每新建一個對象,都相當于本地賦值980次。參考“重復利用對象”(下一節)、Van Wyk[12] p.87以及Bentley[15] p.81 |
| `x/2`(或2的任意次冪) | `X>>2`(或2的任意次冪) | 使用更快的硬件指令 |
### D.3.3 特殊情況
+ 字符串的開銷:字符串連接運算符`+`看似簡單,但實際需要消耗大量系統資源。編譯器可高效地連接字符串,但變量字符串卻要求可觀的處理器時間。例如,假設`s`和`t`是字符串變量:
```
System.out.println("heading" + s + "trailer" + t);
```
上述語句要求新建一個`StringBuffer`(字符串緩沖),追加參數,然后用`toString()`將結果轉換回一個字符串。因此,無論磁盤空間還是處理器時間,都會受到嚴重消耗。若準備追加多個字符串,則可考慮直接使用一個字符串緩沖——特別是能在一個循環里重復利用它的時候。通過在每次循環里禁止新建一個字符串緩沖,可節省980單位的對象創建時間(如前所述)。利用`substring()`以及其他字符串方法,可進一步地改善性能。如果可行,字符數組的速度甚至能夠更快。也要注意由于同步的關系,所以`StringTokenizer`會造成較大的開銷。
+ 同步:在JDK解釋器中,調用同步方法通常會比調用不同步方法慢10倍。經JIT編譯器處理后,這一性能上的差距提升到50到100倍(注意前表總結的時間顯示出要慢97倍)。所以要盡可能避免使用同步方法——若不能避免,方法的同步也要比代碼塊的同步稍快一些。
+ 重復利用對象:要花很長的時間來新建一個對象(根據前表總結的時間,對象的新建時間是賦值時間的980倍,而新建一個小數組的時間是賦值時間的3100倍)。因此,最明智的做法是保存和更新老對象的字段,而不是創建一個新對象。例如,不要在自己的`paint()`方法中新建一個`Font`對象。相反,應將其聲明成實例對象,再初始化一次。在這以后,可在`paint()`里需要的時候隨時進行更新。參見Bentley編著的《編程珠璣》,p.81[15]。
+ 異常:只有在不正常的情況下,才應放棄異常處理模塊。什么才叫“不正常”呢?這通常是指程序遇到了問題,而這一般是不愿見到的,所以性能不再成為優先考慮的目標。進行優化時,將小的`try-catch`塊合并到一起。由于這些塊將代碼分割成小的、各自獨立的片斷,所以會妨礙編譯器進行優化。另一方面,若過份熱衷于刪除異常處理模塊,也可能造成代碼健壯程度的下降。
+ 散列處理:首先,Java 1.0和1.1的標準“散列表”(`Hashtable`)類需要轉換以及特別消耗系統資源的同步處理(570單位的賦值時間)。其次,早期的JDK庫不能自動決定最佳的表格尺寸。最后,散列函數應針對實際使用項(`Key`)的特征設計。考慮到所有這些原因,我們可特別設計一個散列類,令其與特定的應用程序配合,從而改善常規散列表的性能。注意Java 1.2集合庫的散列映射(`HashMap`)具有更大的靈活性,而且不會自動同步。
+ 方法內嵌:只有在方法屬于`final`(最終)、`private`(專用)或`static`(靜態)的情況下,Java編譯器才能內嵌這個方法。而且某些情況下,還要求它絕對不可以有局部變量。若代碼花大量時間調用一個不含上述任何屬性的方法,那么請考慮為其編寫一個`final`版本。
+ I/O:應盡可能使用緩沖。否則,最終也許就是一次僅輸入/輸出一個字節的惡果。注意JDK 1.0的I/O類采用了大量同步措施,所以若使用象`readFully()`這樣的一個“大批量”調用,然后由自己解釋數據,就可獲得更佳的性能。也要注意Java 1.1的`reader`和`writer`類已針對性能進行了優化。
+ 轉換和實例:轉換會耗去2到200個單位的賦值時間。開銷更大的甚至要求上溯繼承(遺傳)結構。其他高代價的操作會損失和恢復更低層結構的能力。
+ 圖形:利用剪切技術,減少在`repaint()`中的工作量;倍增緩沖區,提高接收速度;同時利用圖形壓縮技術,縮短下載時間。來自JavaWorld的“Java Applets”以及來自Sun的“Performing Animation”是兩個很好的教程。請記著使用最貼切的命令。例如,為根據一系列點畫一個多邊形,和`drawLine()`相比,`drawPolygon()`的速度要快得多。如必須畫一條單像素粗細的直線,`drawLine(x,y,x,y)`的速度比`fillRect(x,y,1,1)`快。
+ 使用API類:盡量使用來自Java API的類,因為它們本身已針對機器的性能進行了優化。這是用Java難于達到的。比如在復制任意長度的一個數組時,`arraryCopy()`比使用循環的速度快得多。
+ 替換API類:有些時候,API類提供了比我們希望更多的功能,相應的執行時間也會增加。因此,可定做特別的版本,讓它做更少的事情,但可更快地運行。例如,假定一個應用程序需要一個容器來保存大量數組。為加快執行速度,可將原來的`Vector`(向量)替換成更快的動態對象數組。
(1) 其他建議
+ 將重復的常數計算移至關鍵循環之外——比如計算固定長度緩沖區的`buffer.length`。
+ `static final`(靜態最終)常數有助于編譯器優化程序。
+ 實現固定長度的循環。
+ 使用`javac`的優化選項:`-O`。它通過內嵌`static`,`final`以及`private`方法,從而優化編譯過的代碼。注意類的長度可能會增加(只對JDK 1.1而言——更早的版本也許不能執行字節查證)。新型的“Just-in-time”(JIT)編譯器會動態加速代碼。
+ 盡可能地將計數減至0——這使用了一個特殊的JVM字節碼。
## D.4 參考資源
### D.4.1 性能工具
[1] 運行于Pentium Pro 200,Netscape 3.0,JDK 1.1.4的MicroBenchmark(參見下面的參考資源[5])
[2] Sun的Java文檔頁——JDK Java解釋器主題:
http://java.sun.com/products/JDK/tools/win32/java.html
[3] Vladimir Bulatov的HyperProf
http://www.physics.orst.edu/~bulatov/HyperProf
[4] Greg White的ProfileViewer
http://www.inetmi.com/~gwhi/ProfileViewer/ProfileViewer.html
### D.4.2 Web站點
[5] 對于Java代碼的優化主題,最出色的在線參考資源是Jonathan Hardwick的“Java Optimization”網站:
http://www.cs.cmu.edu/~jch/java/optimization.html
“Java優化工具”主頁:
http://www.cs.cmu.edu/~jch/java/tools.html
以及“Java Microbenchmarks”(有一個45秒鐘的評測過程):
http://www.cs.cmu.edu/~jch/java/benchmarks.html
### D.4.3 文章
[6] “Make Java fast:Optimize! How to get the greatest performanceout of your code through low-level optimizations in Java”(讓Java更快:優化!如何通過在Java中的低級優化,使代碼發揮最出色的性能)。作者:Doug Bell。網址:
http://www.javaworld.com/javaworld/jw-04-1997/jw-04-optimize.html
(含一個全面的性能評測程序片,有詳盡注釋)
[7] “Java Optimization Resources”(Java優化資源)
http://www.cs.cmu.edu/~jch/java/resources.html
[8] “Optimizing Java for Speed”(優化Java,提高速度):
http://www.cs.cmu.edu/~jch/java/speed.html
[9] “An Empirical Study of FORTRAN Programs”(FORTRAN程序實戰解析)。作者:Donald Knuth。1971年出版。第1卷,p.105-33,“軟件——實踐和練習”。
[10] “Building High-Performance Applications and Servers in Java:An Experiential Study”。作者:Jimmy Nguyen,Michael Fraenkel,RichardRedpath,Binh Q. Nguyen以及Sandeep K. Singhal。IBM T.J. Watson ResearchCenter,IBM Software Solutions。
http://www.ibm.com/java/education/javahipr.html
### D.4.4 Java專業書籍
[11] 《Advanced Java,Idioms,Pitfalls,Styles, and Programming Tips》。作者:Chris Laffra。Prentice Hall 1997年出版(Java 1.0)。第11章第20小節。
### D.4.5 一般書籍
[12] 《Data Structures and C Programs》(數據結構和C程序)。作者:J.Van Wyk。Addison-Wesly 1998年出版。
[13] 《Writing Efficient Programs》(編寫有效的程序)。作者:Jon Bentley。Prentice Hall 1982年出版。特別參考p.110和p.145-151。
[14] 《More Programming Pearls》(編程珠璣第二版)。作者:JonBentley。“Association for Computing Machinery”,1998年2月。
[15] 《Programming Pearls》(編程珠璣)。作者:Jone Bentley。Addison-Wesley 1989年出版。第2部分強調了常規的性能改善問題。 [16] 《Code Complete:A Practical Handbook of Software Construction》(完整代碼索引:實用軟件開發手冊)。作者:Steve McConnell。Microsoft出版社1993年出版,第9章。
[17] 《Object-Oriented System Development》(面向對象系統的開發)。作者:Champeaux,Lea和Faure。第25章。
[18] 《The Art of Programming》(編程藝術)。作者:Donald Knuth。第1卷“基本算法第3版”;第3卷“排序和搜索第2版”。Addison-Wesley出版。這是有關程序算法的一本百科全書。
[19] 《Algorithms in C:Fundammentals,Data Structures, Sorting,Searching》(C算法:基礎、數據結構、排序、搜索)第3版。作者:RobertSedgewick。Addison-Wesley 1997年出版。作者是Knuth的學生。這是專門討論幾種語言的七個版本之一。對算法進行了深入淺出的解釋。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物