有三條與優(yōu)化有關(guān)的格言是每個(gè)人都應(yīng)該知道的。這些格言我們可能已經(jīng)耳熟能詳,但是,如果對(duì)它們還不太熟悉失乾,請(qǐng)看下面:
很多計(jì)算上的過(guò)失都被歸咎于效率(沒(méi)有必要達(dá)到的效率),而不是任何其他的原因纬乍,——甚至包括盲目地做傻事碱茁。
——William A.Wulf[Wulf72]
不要去計(jì)校效率上的一些小小的得失,在97%的情況下仿贬。不成熟的優(yōu)化才是一切問(wèn)題的根源纽竣。
——Donald E.Knuth[Knuth74]
在優(yōu)化方面,我們應(yīng)該遵守兩條規(guī)則:
規(guī)則1:不要進(jìn)行優(yōu)化。
規(guī)則2(僅針對(duì)專(zhuān)加):還是不要進(jìn)行優(yōu)化一一也就是說(shuō)蜓氨,在你還沒(méi)有絕對(duì)清晰的未優(yōu)化方案之前聋袋,請(qǐng)不要進(jìn)行優(yōu)化.
——M.A.Jackson[Jackson75]
所有這些格言都比Java程序設(shè)計(jì)語(yǔ)言的出現(xiàn)早了2 0年。它們講述了一個(gè)關(guān)于優(yōu)化的深刻真理:優(yōu)化的弊大于利语盈,特別是不成熟的優(yōu)化舱馅。在優(yōu)化過(guò)程中,產(chǎn)生的軟件可能既不快速刀荒,也不正確代嗤,而且還不容易修正。
不要因?yàn)樾阅芏鵂奚侠淼慕Y(jié)構(gòu)缠借。要努力編寫(xiě)好的程序而不足快的欄序干毅。如果好的程序不夠快,它的結(jié)構(gòu)將使它可以得到優(yōu)化泼返。好的程序體現(xiàn)了信息隱藏 (information hiding)的原則:只要有可能硝逢,它們就會(huì)把設(shè)計(jì)決策集中在單個(gè)模塊中,因此绅喉,可以改變單個(gè)決策渠鸽,而不會(huì)影響到系統(tǒng)的其他部分(見(jiàn)第13條:使類(lèi)和成員的可訪(fǎng)問(wèn)性最小化)。
這并不意味著在完成程序之前就可以忽略性能問(wèn)題柴罐。實(shí)現(xiàn)上的問(wèn)題可以通過(guò)后期的優(yōu)化而得到修正徽缚,但是,遍布全局并且限制性能的結(jié)構(gòu)缺陷幾乎是不可能被改正的.除非重新編寫(xiě)系統(tǒng)革屠。在系統(tǒng)完成之后再改變?cè)O(shè)計(jì)的某個(gè)基本方面凿试,會(huì)導(dǎo)致系統(tǒng)的結(jié)構(gòu)很不好,從而難以維護(hù)和改進(jìn)似芝。因此那婉,必須在設(shè)計(jì)過(guò)程中考慮到性能間題。
努力避免那些限制性能的設(shè)計(jì)決策党瓮。當(dāng)一個(gè)系統(tǒng)設(shè)計(jì)完成之后详炬,其中最難以更改的組件是那些指定了模塊之間交互關(guān)系以及模塊與外界交互關(guān)系的組件。在這些設(shè)計(jì)組件之中寞奸,最主要的是API線(xiàn)路層(wire-Ievel)協(xié)議以及永久數(shù)據(jù)格式痕寓。這些設(shè)計(jì)組件不僅在事后難以甚至不可能改變,而且它們都有可能對(duì)系統(tǒng)本該達(dá)到的性能產(chǎn)生嚴(yán)重的限制蝇闭。
要考慮API設(shè)計(jì)決策的性能后果。使公有的類(lèi)型成為可變的(mutable )硬毕。這可能會(huì)導(dǎo)致大量不必要的保護(hù)性拷貝(見(jiàn)第39條:必要時(shí)進(jìn)行保護(hù)性拷貝 )呻引。同樣地,在適合使用復(fù)合模式的公有類(lèi)中使用繼承吐咳,會(huì)把這個(gè)類(lèi)與它的超類(lèi)永遠(yuǎn)地束縛在一起逻悠,從而人為地限制了子類(lèi)的性能(見(jiàn)第16條:復(fù)合優(yōu)先于繼承)元践。最后一個(gè)例子,在A(yíng)PI中使用實(shí)現(xiàn)類(lèi)型而不是接口童谒,會(huì)把你束縛在一個(gè)具休的實(shí)現(xiàn)上单旁,即使將來(lái)出現(xiàn)更快的實(shí)現(xiàn)你也無(wú)法使用(見(jiàn)第52條:通過(guò)接口引用對(duì)象)。
API設(shè)計(jì)對(duì)于性能的影響是非常實(shí)際的饥伊∠蠡耄考慮java.awt.Component類(lèi)中的getSize方法。這個(gè)決定就是琅豆,這個(gè)注重性能的方法將返回Dimension實(shí)例愉豺,與此密切相關(guān)的決定是,Dimension實(shí)例是可變的茫因,迫使這個(gè)方法的任何實(shí)現(xiàn)都必須為每個(gè)調(diào)用分配一個(gè)新的Dimension實(shí)例蚪拦。盡管在現(xiàn)代V M上分配小對(duì)象的開(kāi)銷(xiāo)并不大,但是分配數(shù)百萬(wàn)個(gè)不必要的對(duì)象仍然會(huì)嚴(yán)重地?fù)p害性能冻押。
public Dimension getSize() {
return size();
}
@Deprecated
public Dimension size() {
return new Dimension(width, height);
}
在這種情況下驰贷,有幾種可供選擇的替換方案。理想情況下洛巢,Dimension應(yīng)該是不可變的(見(jiàn)第15條:使可變性最小化)括袒;另一種方案是,用兩個(gè)方法來(lái)替換getSize方法狼渊,它們分別返回Dimension對(duì)象的單個(gè)基本組件箱熬。實(shí)際上.在1.2發(fā)行版本中,出于性能方面的原因狈邑,兩個(gè)這樣的方法已經(jīng)被加入到Component API中城须。然而,原先的客戶(hù)端代碼仍然可以使用getSize方法米苹,但是仍然要承受原始API設(shè)計(jì)決策所帶來(lái)的性能影響糕伐。
幸運(yùn)的是,一般而言良瞧,好的API設(shè)計(jì)也會(huì)帶來(lái)好的性能褥蚯。為獲得好的性能而對(duì)API進(jìn)行包裝赞庶,這是一種非常不好的想法澜薄。導(dǎo)致你對(duì)API進(jìn)行包裝的性能因素可能會(huì)在平臺(tái)未來(lái)的發(fā)行版本中肤京,或者在將來(lái)的底層軟件中不復(fù)存在忘分,但是披包裝的API以及由它引起的問(wèn)題將永遠(yuǎn)困擾著你。
一旦謹(jǐn)慎地設(shè)計(jì)了程序熬荆,井且產(chǎn)生了一個(gè)清晰累盗、簡(jiǎn)明若债、結(jié)構(gòu)良好的實(shí)現(xiàn),那么就到了該考慮優(yōu)化的時(shí)候了傲须,假定此時(shí)你對(duì)于程序的性能還不滿(mǎn)意泰讽。
回想一下Jackson的兩條優(yōu)化規(guī)則:“不要優(yōu)化"以及“(僅針對(duì)專(zhuān)家)還是不要優(yōu)化"。他可以再增加一條:在每次試圖做優(yōu)化之前和之后,要對(duì)性能進(jìn)行測(cè)量永乌。你可能會(huì)驚訝于自己的發(fā)現(xiàn)翅雏。試圖做的優(yōu)化通常對(duì)于性能井沒(méi)有明顯的影響望几,有時(shí)候甚至?xí)剐阅茏兊酶睢V饕脑蛟谟诼ナ模鲁龀绦虬褧r(shí)間花在哪些地方并不容易名挥。你認(rèn)為程序慢的地方可能井沒(méi)有問(wèn)題榄融,這種情況下實(shí)際上是在浪費(fèi)時(shí)間去嘗試優(yōu)化愧杯。大多數(shù)人認(rèn)為:程序把8 0%的時(shí)間花在2 0%的代碼上了力九。
性能剖析工具有助于你決定應(yīng)該把優(yōu)化的重心放在哪里跌前。這樣的工具可以為你提供運(yùn)行時(shí)的信息,比如每個(gè)方法大致上花費(fèi)了多少時(shí)間臂寝、它被調(diào)用多少次。除了確定優(yōu)化的重點(diǎn)之外帚呼,它還可以警告你是否需要改變算法眷蜈。如果一個(gè)平方級(jí)(或更差)的算法潛藏在程序中酌儒,無(wú)論怎么調(diào)整和優(yōu)化都很難解決問(wèn)題。你必須用更有效的算法來(lái)替換原來(lái)的算法。系統(tǒng)中的代碼越多,使用性能剖析器就顯得越發(fā)重要酝润。這就好像要在一堆干草中尋找一根針:達(dá)堆干草越大,使用金屬探測(cè)器就越有用璃弄。JDK帶了簡(jiǎn)單的性能剖析器要销,現(xiàn)代的IDE也提供了更加成熟的性能剖折工具。
在Java乎臺(tái)上對(duì)優(yōu)化的結(jié)果進(jìn)行測(cè)量夏块,比在其他的傳統(tǒng)平臺(tái)上更有必要疏咐,因?yàn)镴ava程序設(shè)計(jì)語(yǔ)言沒(méi)有很強(qiáng)的性能模型 。各種基本操作的相對(duì)開(kāi)銷(xiāo)也沒(méi)有明確定義脐供。程序員所編寫(xiě)的代碼與CPU執(zhí)行的代碼之間存在“語(yǔ)義溝(semantic gap)”浑塞,而且這條語(yǔ)義溝比傳統(tǒng)編譯語(yǔ)言中的更大,這使得要想可靠地預(yù)測(cè)出任何優(yōu)化的性能結(jié)果都非常困難政己。大量流傳的關(guān)于性能的說(shuō)法最終都被證明為半真半假酌壕。或者根本就不正確歇由。
不僅Java的性能模型未得到很好的定義卵牍。而且在不同的JVM實(shí)現(xiàn).,或者不同的發(fā)行版本沦泌,以及不同的處理器糊昙,在它們這些當(dāng)中也都各不相同。如果將要在多個(gè)JVM實(shí)現(xiàn)和多種硬件平臺(tái)上運(yùn)行程序谢谦,很重要的一點(diǎn)是释牺,需要在每個(gè)Java實(shí)現(xiàn)上測(cè)量?jī)?yōu)化效果萝衩。有時(shí)候,還必須在從不同JVM實(shí)現(xiàn)或者硬件平臺(tái)上得到的性能結(jié)果之中進(jìn)行權(quán)衡没咙。
總而言之猩谊,不要費(fèi)力去編寫(xiě)快速的程序——應(yīng)該努力編寫(xiě)好的程序,速度自然會(huì)隨之而來(lái)祭刚。在設(shè)計(jì)系統(tǒng)的時(shí)候预柒,特別是在設(shè)計(jì)API、線(xiàn)路層協(xié)議和永久數(shù)據(jù)格式的時(shí)候袁梗,一定要考慮性能的因素。當(dāng)構(gòu)建完系統(tǒng)之后憔古,要測(cè)量它的性能遮怜。如果它足夠快,你的任務(wù)就完成了鸿市。如果不夠快锯梁,則可以在性能剖析器的幫助下,找到問(wèn)題的根源焰情,然后設(shè)法優(yōu)化系統(tǒng)中相關(guān)的部分陌凳。第一個(gè)步驟是檢查所選擇的算法:再多的低層優(yōu)化也無(wú)法彌補(bǔ)算法的選擇不當(dāng)。必要時(shí)重復(fù)這個(gè)過(guò)程内舟,在每次改變之后都要測(cè)量性能合敦,直到滿(mǎn)意為止。