“本附錄由Joe Sharp投稿,并獲得他的同意在這兒轉載。請聯(lián)系SharpJoe@aol.com”
Java語(yǔ)言特別強調準確性,但可靠的行為要以性能作為代價(jià)。這一特點(diǎn)反映在自動(dòng)收集垃圾、嚴格的運行期檢查、完整的字節碼檢查以及保守的運行期同步等等方面。對一個(gè)解釋型的虛擬機來(lái)說(shuō),由于目前有大量平臺可供挑選,所以進(jìn)一步阻礙了性能的發(fā)揮。
“先做完它,再逐步完善。幸好需要改進(jìn)的地方通常不會(huì )太多?!保⊿teve McConnell的《About performance》[16])
本附錄的宗旨就是指導大家尋找和優(yōu)化“需要完善的那一部分”。
D.1 基本方法
只有正確和完整地檢測了程序后,再可著(zhù)手解決性能方面的問(wèn)題:
(1) 在現實(shí)環(huán)境中檢測程序的性能。若符合要求,則目標達到。若不符合,則轉到下一步。
(2) 尋找最致命的性能瓶頸。這也許要求一定的技巧,但所有努力都不會(huì )白費。如簡(jiǎn)單地猜測瓶頸所在,并試圖進(jìn)行優(yōu)化,那么可能是白花時(shí)間。
(3) 運用本附錄介紹的提速技術(shù),然后返回步驟1。
為使努力不至白費,瓶頸的定位是至關(guān)重要的一環(huán)。Donald Knuth[9]曾改進(jìn)過(guò)一個(gè)程序,那個(gè)程序把50%的時(shí)間都花在約4%的代碼量上。在僅一個(gè)工作小時(shí)里,他修改了幾行代碼,使程序的執行速度倍增。此時(shí),若將時(shí)間繼續投入到剩余代碼的修改上,那么只會(huì )得不償失。Knuth在編程界有一句名言:“過(guò)早的優(yōu)化是一切麻煩的根源”(Premature optimization is the root of all evil)。最明智的做法是抑制過(guò)早優(yōu)化的沖動(dòng),因為那樣做可能遺漏多種有用的編程技術(shù),造成代碼更難理解和操控,并需更大的精力進(jìn)行維護。
D.2 尋找瓶頸
為找出最影響程序性能的瓶頸,可采取下述幾種方法:
D.2.1 安插自己的測試代碼
插入下述“顯式”計時(shí)代碼,對程序進(jìn)行評測:
long start = System.currentTimeMillis();
// 要計時(shí)的運算代碼放在這兒
long time = System.currentTimeMillis() - start;
利用System.out.println(),讓一種不常用到的方法將累積時(shí)間打印到控制臺窗口。由于一旦出錯,編譯器會(huì )將其忽略,所以可用一個(gè)“靜態(tài)最終布爾值”(Static final boolean)打開(kāi)或關(guān)閉計時(shí),使代碼能放心留在最終發(fā)行的程序里,這樣任何時(shí)候都可以拿來(lái)應急。盡管還可以選用更復雜的評測手段,但若僅僅為了量度一個(gè)特定任務(wù)的執行時(shí)間,這無(wú)疑是最簡(jiǎn)便的方法。
System.currentTimeMillis()返回的時(shí)間以千分之一秒(1毫秒)為單位。然而,有些系統的時(shí)間精度低于1毫秒(如Windows PC),所以需要重復n次,再將總時(shí)間除以n,獲得準確的時(shí)間。
D.2.2 JDK性能評測[2]
JDK配套提供了一個(gè)內建的評測程序,能跟蹤花在每個(gè)例程上的時(shí)間,并將評測結果寫(xiě)入一個(gè)文件。不幸的是,JDK評測器并不穩定。它在JDK 1.1.1中能正常工作,但在后續版本中卻非常不穩定。
為運行評測程序,請在調用Java解釋器的未優(yōu)化版本時(shí)加上-prof選項。例如:
java_g -prof myClass
或加上一個(gè)程序片(Applet):
java_g -prof sun.applet.AppletViewer applet.html
理解評測程序的輸出信息并不容易。事實(shí)上,在JDK 1.0中,它居然將方法名稱(chēng)截短為30字符。所以可能無(wú)法區分出某些方法。然而,若您用的平臺確實(shí)能支持-prof選項,那么可試試Vladimir Bulatov的“HyperPorf”[3]或者Greg White的“ProfileViewer”來(lái)解釋一下結果。
D.2.3 特殊工具
如果想隨時(shí)跟上性能優(yōu)化工具的潮流,最好的方法就是作一些Web站點(diǎn)的???。比如由Jonathan Hardwick制作的“Tools for Optimizing Java”(Java優(yōu)化工具)網(wǎng)站:
http://www.cs.cmu.edu/~jch/java/tools.html
D.2.4 性能評測的技巧
■由于評測時(shí)要用到系統時(shí)鐘,所以當時(shí)不要運行其他任何進(jìn)程或應用程序,以免影響測試結果。
■如對自己的程序進(jìn)行了修改,并試圖(至少在開(kāi)發(fā)平臺上)改善它的性能,那么在修改前后應分別測試一下代碼的執行時(shí)間。
■盡量在完全一致的環(huán)境中進(jìn)行每一次時(shí)間測試。
■如果可能,應設計一個(gè)不依賴(lài)任何用戶(hù)輸入的測試,避免用戶(hù)的不同反應導致結果出現誤差。
D.3 提速方法
現在,關(guān)鍵的性能瓶頸應已隔離出來(lái)。接下來(lái),可對其應用兩種類(lèi)型的優(yōu)化:常規手段以及依賴(lài)Java語(yǔ)言。
D.3.1 常規手段
通常,一個(gè)有效的提速方法是用更現實(shí)的方式重新定義程序。例如,在《Programming Pearls》(編程拾貝)一書(shū)中[14],Bentley利用了一段小說(shuō)數據描寫(xiě),它可以生成速度非???、而且非常精簡(jiǎn)的拼寫(xiě)檢查器,從而介紹了Doug McIlroy對英語(yǔ)語(yǔ)言的表述。除此以外,與其他方法相比,更好的算法也許能帶來(lái)更大的性能提升??特別是在數據集的尺寸越來(lái)越大的時(shí)候。欲了解這些常規手段的詳情,請參考本附錄末尾的“一般書(shū)籍”清單。
D.3.2 依賴(lài)語(yǔ)言的方法
為進(jìn)行客觀(guān)的分析,最好明確掌握各種運算的執行時(shí)間。這樣一來(lái),得到的結果可獨立于當前使用的計算機??通過(guò)除以花在本地賦值上的時(shí)間,最后得到的就是“標準時(shí)間”。
運算 示例 標準時(shí)間
本地賦值 i=n; 1.0
實(shí)例賦值 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
空循環(huán) while(true) n++; 2.0
三元表達式 (x<0) ?-x : x 2.2
算術(shù)調用 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
通過(guò)自己的系統(如我的Pentium 200 Pro,Netscape 3及JDK 1.1.5),這些相對時(shí)間向大家揭示出:新建對象和數組會(huì )造成最沉重的開(kāi)銷(xiāo),同步會(huì )造成比較沉重的開(kāi)銷(xiāo),而一次不同步的方法調用會(huì )造成適度的開(kāi)銷(xiāo)。參考資源[5]和[6]為大家總結了測量用程序片的Web地址,可到自己的機器上運行它們。
1. 常規修改
下面是加快Java程序關(guān)鍵部分執行速度的一些常規操作建議(注意對比修改前后的測試結果)。
將... 修改成... 理由
接口 抽象類(lèi)(只需一個(gè)父時(shí)) 接口的多個(gè)繼承會(huì )妨礙性能的優(yōu)化
非本地或數組循環(huán)變量 本地循環(huán)變量 根據前表的耗時(shí)比較,一次實(shí)例整數賦值的時(shí)間是本地整數賦值時(shí)間的1.2倍,但數組賦值的時(shí)間是本地整數賦值的2.7倍
鏈接列表(固定尺寸) 保存丟棄的鏈接項目,或將列表替換成一個(gè)循環(huán)數組(大致知道尺寸) 每新建一個(gè)對象,都相當于本地賦值980次。參考“重復利用對象”(下一節)、Van Wyk[12] p.87以及Bentley[15] p.81
x/2(或2的任意次冪) X>>2(或2的任意次冪) 使用更快的硬件指令
D.3.3 特殊情況
■字串的開(kāi)銷(xiāo):字串連接運算符+看似簡(jiǎn)單,但實(shí)際需要消耗大量系統資源。編譯器可高效地連接字串,但變量字串卻要求可觀(guān)的處理器時(shí)間。例如,假設s和t是字串變量:
System.out.println("heading" + s + "trailer" + t);
上述語(yǔ)句要求新建一個(gè)StringBuffer(字串緩沖),追加自變量,然后用toString()將結果轉換回一個(gè)字串。因此,無(wú)論磁盤(pán)空間還是處理器時(shí)間,都會(huì )受到嚴重消耗。若準備追加多個(gè)字串,則可考慮直接使用一個(gè)字串緩沖??特別是能在一個(gè)循環(huán)里重復利用它的時(shí)候。通過(guò)在每次循環(huán)里禁止新建一個(gè)字串緩沖,可節省980單位的對象創(chuàng )建時(shí)間(如前所述)。利用substring()以及其他字串方法,可進(jìn)一步地改善性能。如果可行,字符數組的速度甚至能夠更快。也要注意由于同步的關(guān)系,所以StringTokenizer會(huì )造成較大的開(kāi)銷(xiāo)。
■同步:在JDK解釋器中,調用同步方法通常會(huì )比調用不同步方法慢10倍。經(jīng)JIT編譯器處理后,這一性能上的差距提升到50到100倍(注意前表總結的時(shí)間顯示出要慢97倍)。所以要盡可能避免使用同步方法??若不能避免,方法的同步也要比代碼塊的同步稍快一些。
■重復利用對象:要花很長(cháng)的時(shí)間來(lái)新建一個(gè)對象(根據前表總結的時(shí)間,對象的新建時(shí)間是賦值時(shí)間的980倍,而新建一個(gè)小數組的時(shí)間是賦值時(shí)間的3100 倍)。因此,最明智的做法是保存和更新老對象的字段,而不是創(chuàng )建一個(gè)新對象。例如,不要在自己的paint()方法中新建一個(gè)Font對象。相反,應將其聲明成實(shí)例對象,再初始化一次。在這以后,可在paint()里需要的時(shí)候隨時(shí)進(jìn)行更新。參見(jiàn)Bentley編著(zhù)的《編程拾貝》,p.81[15]。
■異常:只有在不正常的情況下,才應放棄異常處理模塊。什么才叫“不正?!蹦??這通常是指程序遇到了問(wèn)題,而這一般是不愿見(jiàn)到的,所以性能不再成為優(yōu)先考慮的目標。進(jìn)行優(yōu)化時(shí),將小的“try-catch”塊合并到一起。由于這些塊將代碼分割成小的、各自獨立的片斷,所以會(huì )妨礙編譯器進(jìn)行優(yōu)化。另一方面,若過(guò)份熱衷于刪除異常處理模塊,也可能造成代碼健壯程度的下降。
■散列處理:首先,Java 1.0和1.1的標準“散列表”(Hashtable)類(lèi)需要造型以及特別消耗系統資源的同步處理(570單位的賦值時(shí)間)。其次,早期的JDK庫不能自動(dòng)決定最佳的表格尺寸。最后,散列函數應針對實(shí)際使用項(Key)的特征設計??紤]到所有這些原因,我們可特別設計一個(gè)散列類(lèi),令其與特定的應用程序配合,從而改善常規散列表的性能。注意Java 1.2集合庫的散列映射(HashMap)具有更大的靈活性,而且不會(huì )自動(dòng)同步。
■方法內嵌:只有在方法屬于final(最終)、private(專(zhuān)用)或static(靜態(tài))的情況下,Java編譯器才能內嵌這個(gè)方法。而且某些情況下,還要求它絕對不可以有局部變量。若代碼花大量時(shí)間調用一個(gè)不含上述任何屬性的方法,那么請考慮為其編寫(xiě)一個(gè)“final”版本。
■I/O:應盡可能使用緩沖。否則,最終也許就是一次僅輸入/輸出一個(gè)字節的惡果。注意JDK 1.0的I/O類(lèi)采用了大量同步措施,所以若使用象readFully()這樣的一個(gè)“大批量”調用,然后由自己解釋數據,就可獲得更佳的性能。也要注意Java 1.1的“reader”和“writer”類(lèi)已針對性能進(jìn)行了優(yōu)化。
■造型和實(shí)例:造型會(huì )耗去2到200個(gè)單位的賦值時(shí)間。開(kāi)銷(xiāo)更大的甚至要求上溯繼承(遺傳)結構。其他高代價(jià)的操作會(huì )損失和恢復更低層結構的能力。
■圖形:利用剪切技術(shù),減少在repaint()中的工作量;倍增緩沖區,提高接收速度;同時(shí)利用圖形壓縮技術(shù),縮短下載時(shí)間。來(lái)自JavaWorld的 “Java Applets”以及來(lái)自Sun的“Performing Animation”是兩個(gè)很好的教程。請記著(zhù)使用最貼切的命令。例如,為根據一系列點(diǎn)畫(huà)一個(gè)多邊形,和drawLine()相比, drawPolygon()的速度要快得多。如必須畫(huà)一條單像素粗細的直線(xiàn),drawLine(x,y,x,y)的速度比f(wàn)illRect(x,y, 1,1)快。
■使用API類(lèi):盡量使用來(lái)自Java API的類(lèi),因為它們本身已針對機器的性能進(jìn)行了優(yōu)化。這是用Java難于達到的。比如在復制任意長(cháng)度的一個(gè)數組時(shí),arraryCopy()比使用循環(huán)的速度快得多。
■替換API類(lèi):有些時(shí)候,API類(lèi)提供了比我們希望更多的功能,相應的執行時(shí)間也會(huì )增加。因此,可定做特別的版本,讓它做更少的事情,但可更快地運行。例如,假定一個(gè)應用程序需要一個(gè)容器來(lái)保存大量數組。為加快執行速度,可將原來(lái)的Vector(矢量)替換成更快的動(dòng)態(tài)對象數組。
1. 其他建議
■將重復的常數計算移至關(guān)鍵循環(huán)之外??比如計算固定長(cháng)度緩沖區的buffer.length。
■static final(靜態(tài)最終)常數有助于編譯器優(yōu)化程序。
■實(shí)現固定長(cháng)度的循環(huán)。
■使用javac的優(yōu)化選項:-O。它通過(guò)內嵌static,final以及private方法,從而優(yōu)化編譯過(guò)的代碼。注意類(lèi)的長(cháng)度可能會(huì )增加(只對JDK 1.1而言??更早的版本也許不能執行字節查證)。新型的“Just-in-time”(JIT)編譯器會(huì )動(dòng)態(tài)加速代碼。
■盡可能地將計數減至0??這使用了一個(gè)特殊的JVM字節碼。
D.4 參考資源
D.4.1 性能工具
[1] 運行于Pentium Pro 200,Netscape 3.0,JDK 1.1.4的MicroBenchmark(參見(jiàn)下面的參考資源[5])
[2] Sun的Java文檔頁(yè)??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站點(diǎn)
[5] 對于Java代碼的優(yōu)化主題,最出色的在線(xiàn)參考資源是Jonathan Hardwick的“Java Optimization”網(wǎng)站:
http://www.cs.cmu.edu/~jch/java/optimization.html
“Java優(yōu)化工具”主頁(yè):
http://www.cs.cmu.edu/~jch/java/tools.html
以及“Java Microbenchmarks”(有一個(gè)45秒鐘的評測過(guò)程):
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更快:優(yōu)化!如何通過(guò)在Java中的低級優(yōu)化,使代碼發(fā)揮最出色的性能)。作者:Doug Bell。網(wǎng)址:
http://www.javaworld.com/javaworld/jw-04-1997/jw-04-optimize.html
(含一個(gè)全面的性能評測程序片,有詳盡注釋?zhuān)?br>[7] “Java Optimization Resources”(Java優(yōu)化資源)
http://www.cs.cmu.edu/~jch/java/resources.html
[8] “Optimizing Java for Speed”(優(yōu)化Java,提高速度):
http://www.cs.cmu.edu/~jch/java/speed.html
[9] “An Empirical Study of FORTRAN Programs”(FORTRAN程序實(shí)戰解析)。作者:Donald Knuth。1971年出版。第1卷,p.105-33,“軟件??實(shí)踐和練習”。
[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專(zhuān)業(yè)書(shū)籍
[11] 《Advanced Java,Idioms,Pitfalls,Styles, and Programming Tips》。作者:Chris Laffra。Prentice Hall 1997年出版(Java 1.0)。第11章第20小節。
D.4.5 一般書(shū)籍
[12] 《Data Structures and C Programs》(數據結構和C程序)。作者:J.Van Wyk。Addison-Wesly 1998年出版。
[13] 《Writing Efficient Programs》(編寫(xiě)有效的程序)。作者:Jon Bentley。Prentice Hall 1982年出版。特別參考p.110和p.145-151。
[14] 《More Programming Pearls》(編程拾貝第二版)。作者:JonBentley?!癆ssociation for Computing Machinery”,1998年2月。
[15] 《Programming Pearls》(編程拾貝)。作者:Jone Bentley。Addison-Wesley 1989年出版。第2部分強調了常規的性能改善問(wèn)題。 [16] 《Code Complete:A Practical Handbook of Software Construction》(完整代碼索引:實(shí)用軟件開(kāi)發(fā)手冊)。作者:Steve McConnell。Microsoft出版社1993年出版,第9章。
[17] 《Object-Oriented System Development》(面向對象系統的開(kāi)發(fā))。作者:Champeaux,Lea和Faure。第25章。
[18] 《The Art of Programming》(編程藝術(shù))。作者:Donald Knuth。第1卷“基本算法第3版”;第3卷“排序和搜索第2版”。Addison-Wesley出版。這是有關(guān)程序算法的一本百科全書(shū)。
[19] 《Algorithms in C:Fundammentals,Data Structures, Sorting,Searching》(C算法:基礎、數據結構、排序、搜索)第3版。作者:RobertSedgewick。Addison-Wesley 1997年出版。作者是Knuth的學(xué)生。這是專(zhuān)門(mén)討論幾種語(yǔ)言的七個(gè)版本之一。對算法進(jìn)行了深入淺出的解釋。