- Java性能權威指南(第2版)
- (美)斯科特·奧克斯
- 3644字
- 2022-05-09 14:52:34
1.3 全面的性能
本書重點關注如何最好地利用JVM和Java平臺API,讓程序能運行得更快。但是很多外部因素也會影響性能。這些外部影響因素會時不時地在書中出現,它們并不是Java獨有的,所以不一定會詳細討論。要知道,JVM和Java平臺的性能只是性能優化主題的一小部分。
本節介紹了一些外部影響因素,這些因素和本書討論的Java性能優化話題一樣重要。本書以Java為基礎的內容可以與這些因素互為補充,但其中許多已經超出了我們要討論的范圍。
1.3.1 寫出更好的算法
本書會討論Java影響應用程序性能的很多細節和大量調優標志,只是沒有如-XX:+RunReallyFast的神奇設置。
歸根結底,程序的性能取決于程序怎么寫。如果使用循環遍歷數組中的所有元素,那么JVM可以優化數組的邊界檢查方式,讓循環運行得更快,還可以展開循環操作以提供額外的加速。如果使用循環是為了查找某個特定的元素,那么世界上還沒有可行的優化方式,能讓基于數組的代碼和使用哈希映射(hash map)一樣快。
好的算法對于提升性能是至關重要的。
1.3.2 寫更少的代碼
我們中有些人寫代碼是為了賺錢,有些是為了好玩,有些是為了回饋社區,但我們所有人都在寫代碼,或在寫代碼的團隊中工作。你很難覺得刪減代碼是在為項目做貢獻,因為有些管理者還會用代碼行數來評估開發人員。
我明白這種情況,但矛盾的是,寫得好的小程序比寫得好的大程序跑得快。對于所有的計算機程序都是如此,這自然也適用于Java程序。需要編譯的代碼越多,代碼快速運行之前需要的時間就越長。需要分配后銷毀的對象越多,垃圾回收器需要做的工作就越多。需要分配后持有的對象越多,GC周期就越長。需要從磁盤加載到JVM的類越多,程序開始運行需要的時間就越長。執行過的代碼越多,放入機器的硬件緩存的可能性就越小。需要執行的代碼越多,執行的時間就越長。
我把這概括為“積少成多”原理。開發人員會說,他們只是加了一個非常小的功能,根本用不了多長時間(尤其是不使用該功能的時候),同一個項目中的其他開發人員也這么說。結果是,性能突然下降了幾個百分點。下個版本中再次發生了這種事,現在程序的性能已經降低了10%。這種事發生了幾次之后,性能測試會觸達某個資源的閾值——內存使用到達臨界點、代碼緩存溢出或其他類似的情況。在這些情況下,定期的性能測試可以找出性能下降的原因,性能優化團隊也可以修復導致性能大幅下降的問題。不然隨著時間的推移,小的性能損耗不斷積累,修復就會變得越來越難。
我們最終會輸掉這場戰爭
一個反常(令人沮喪)的地方是,每個應用程序的性能都會隨著時間下降,準確地說是隨著應用程序新版本的發布而下降。這種性能差異常常被忽略,因為硬件的改進可以保證新應用程序的運行速度。
想想看,如果在曾經運行Windows 95的計算機上運行Windows 10會發生什么。我之前最喜歡的一臺計算機是Mac Quadra 950,但是它無法運行macOS Sierra(如果運行起來,相對于Mac OS 7.5它會非常非常慢)。從更低的層面看,Firefox 69.0似乎比前幾個版本運行得快,但這些只是小版本。隨著標簽式瀏覽、同步滾動和安全特性的增加,Firefox要比Mosaic強大得多,但Mosaic加載我本地硬盤上的HTML文件的速度要比Firefox 69.0快50%。
當然,Mosaic已經無法加載幾乎所有主流網站的真實URL了,我們不能再將Mosaic作為主要的瀏覽器了。這也印證了普遍認同的一點,特別是在兩個小版本之間,代碼可以在優化后運行得更快。這是性能優化工程師應該關注的地方。我們擅長這個工作的話,就能贏得戰斗。
這是有用且有價值的事。我的觀點并不是不應該優化現有應用程序的性能。諷刺的是,為了趕超競品,添加新特性和使用新標準只會導致應用程序越來越大、越來越慢。
我不是倡導永遠不應在產品中添加新特性或新代碼,增強程序顯然會帶來好處。但是注意做好權衡,盡可能提高效率。
1.3.3 過早優化
高德納被認為是最早創造過早優化這個詞的人。開發人員經常使用這個詞來宣稱,代碼的性能重要不重要,得運行了才知道。也許你從不知道,完整的原話是這么說的:“大部分時間里,比如在97%的時間里,我們不應該對細枝末節進行優化。過早優化是萬惡之源。”3
3到底是高德納還是Topy Hoare先說的這句話,目前尚有爭議,不過這句話在高德納的文章“Structured Programming with goto Statements”中出現過。在文中,這句話是優化代碼的一個論據,即使這需要類似goto語句這種不太優雅的解決方案。
這句話的意義在于,歸根結底,你應該寫出清晰明了、簡單易懂的代碼。這里的優化指的是,通過改變代碼的算法和設計讓結構復雜的程序也能有更佳的性能。這種類型的優化不應該先做,在分析了程序,發現這樣做能帶來很大益處后再去做。
這里所說的優化并不表示不能使用已知的性能糟糕的代碼結構。如果對于每行代碼都可以在兩種簡單、直接的代碼中做出選擇,那么請選擇性能更好的那個。
在某種程度上,有經驗的Java開發人員都能理解這一點(這是他們在日積月累中學到的一種優化藝術)。思考以下代碼:
log.log(Level.FINE, "I am here, and the value of X is " + calcX() + " and Y is " + calcY());
這段代碼使用的字符串連接看起來沒必要。除非日志級別很高,否則這條消息并不會記錄到日志里。如果日志消息沒有打印,就沒必要調用calcX()方法和calcY()方法。有經驗的Java開發人員會本能地抗拒這樣寫。有些IDE會標記這些代碼并建議對其修改(然而工具是不完美的,NetBeans IDE只會標記字符串連接,不會建議去掉不必要的方法調用)。
改成下面的日志代碼會更好:
if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "I am here, and the value of X is {} and Y is {}", new Object[]{calcX(), calcY()}); }
這完全避免了字符串連接(消息格式不一定更高效,但更簡潔),在不使用日志功能時也避免了方法調用和數組對象的分配。
用這種方式寫的代碼仍然簡潔易讀,但比之前的寫法費不了多少力氣。好吧,我們還是要敲幾下鍵盤并多寫一行邏輯的。但這并不是那種應當避免的過早優化,而是好的程序員都應該學會的技能。
不要讓前輩們那些脫離了上下文的信條阻礙你對自己所寫代碼的思考。你會在本書中看到相關的其他例子,比如第9章討論了一個看起來性能良好的循環結構,該循環用來處理Vector中的元素。
1.3.4 其他:數據庫永遠是瓶頸
如果你在開發不依賴外部資源的獨立Java應用程序,那么該應用程序本身的性能幾乎是最重要的。一旦外部資源(例如數據庫)添加進來,那兩者的性能就都很重要了。在分布式環境中,如果有Java REST服務器、負載均衡器、數據庫和后端企業信息系統,那么Java服務器的性能可能是最不重要的部分。
本書并不關注系統的整體性能。在這樣的環境中,必須對系統的各個方面采取有組織的優化方法。系統的CPU使用率、I/O延遲和各個部分的吞吐量都必須經過測量和分析。只有這樣做,我們才能判斷哪部分造成了系統瓶頸。有關該主題的資源非常豐富,相關的方法和工具也并不只適用于Java。本書假設你已經完成了上述分析,并確定了你的運行環境中需要改進的是Java部分。
bug和性能問題不只局限于JVM
本節只是以數據庫性能為例進行說明,運行環境中的任何部分都可能是性能問題的根源。
我曾經有個客戶在安裝新版本的應用服務器時,遇到測試顯示發送到服務器的請求越來越耗時,我根據奧卡姆剃刀原理考慮了可能導致這個問題的方方面面。
排除了一些因素后,性能問題依然存在。后端數據庫沒問題,所以最有可能出問題的是測試工具。經過測試后,我們發現負載生成器Apache JMeter是問題的源頭。它將所有的響應都保留在一個列表中,當得到新的響應時,它會遍歷整個列表去計算第90百分位響應時間(如果你不熟悉這個名詞,可以查看第2章)。
整個系統的每一部分都可能造成性能問題。常見的案例分析表明,應該首先檢測系統新增加的部分(經常在JVM應用程序里),但仍要準備好檢查運行環境中的每一部分。
另外,不要忽視最初的分析。如果數據庫是瓶頸(提示:它的確是),對訪問數據庫的Java應用程序進行優化不會提升整體性能。實際上,這有可能適得其反。一個通用的準則是,給已經過載的系統增加負載,系統的性能會變差。如果在Java應用程序中做了一些更改讓它更高效,這也會給過載的數據庫增加負載,所以實際上整體性能是下降的。若你就此得出“不應使用某項JVM改進”的結論,那就危險了。
給系統中低效的部分增加負載會讓整個系統變慢,這并不局限于數據庫。例如,將負載添加到CPU密集型服務器上,或者讓更多線程去獲取已經有線程等待的鎖,又或者在其他類似場景中,這個原理都適用。第9章將展示一個僅用了JVM的極端示例。
1.3.5 常見優化
我們很容易將性能的所有方面同等對待,特別是知道“積少成多”的典型表現之后。但我們應該關注常見的用例場景。這一原則體現在以下幾個方面。
·通過分析來優化代碼,并專注于優化其中最耗時的操作。注意,這不代表只需考慮整體里的一部分(見第3章)。
·使用奧卡姆剃刀原理來診斷性能問題。對性能問題最簡單的解釋就是最可能的原因:新增代碼的bug比機器配置更有可能帶來性能問題,而后者比JVM和操作系統的bug更有可能帶來性能問題。操作系統或JVM的確存在潛在的bug,隨著更多確定的因素被排除之后,的確有可能是測試用例在某種情況下觸發了潛在的bug。但不要一開始就考慮這種可能性很小的情況。
·為應用程序的常見操作提供簡單的算法。假設一個應用程序在求解一個數學公式,用戶可以選擇是在10%的誤差范圍內還是1%的誤差范圍內得到答案。如果多數用戶對10%的誤差感到滿意,那就優化代碼路徑——即使這意味著提供具有1%誤差的代碼的速度會變慢。