讀古今文學網 > Java程序員修煉之道 > 通過類加載自動測量 >

通過類加載自動測量

我們在第1章和第5章討論過如何把類編譯成可執行程序。其中一個關鍵步驟是在加載字節碼時進行轉換。這個特性非常強大,是很多現代Java平台的核心技術。其中一個簡單的例子就是方法的自動測量。

在這種方法中,特殊的類加載器加載methodToBeMeasured所屬類,在方法開始和結束的地方加上記錄方法進入和退出時間的字節碼。這些時間通常會被寫入共享的數據結構,由其他線程訪問。這些線程一般會將數據寫入日誌文件,或者通過網絡交給負責處理原始數據的服務器。

很多高端的性能監測工具(比如OpTier CoreFirst)都是以這項技術為核心的。但在編寫本書時,這個市場上似乎還沒有開源工具。

注意 我們會在後面討論到,Java 方法開始時需要進行解釋,然後才切換到編譯模式。要得到真正的性能指標結果,你必須去掉解釋模式佔用的時間,因為它們會嚴重扭曲真實結果。後面還會給出更多細節,告訴你如何確定方法切換為編譯模式的時間。

你可以用這兩項技術(其一或全部)找出某一方法執行所需的時長。下一個問題,完成調優之後,你想得到什麼樣的數值?

6.2.3 知道性能目標是什麼

清晰的目標能讓人注意力集中,所以瞭解和傳達優化的最終目標(知道要測量什麼)至關重要。大多數情況下,這個目標簡單而明確,比如:

  • 將10個並發用戶的端到端等待時間的第90個百分位數減少20%;
  • handleRequest的平均等待時間減少40%,方差減少25%。

在一些更複雜的情況中,目標可能由幾個相關的性能目標共同構成。你要知道,你所測量和想要優化的獨立可觀測項越多,調優工作就會變得越複雜。優化一個性能目標可能會對其他性能目標產生負面影響。

有時,在設定目標之前你很有必要做些初步分析,比如在確定要讓方法運行得更快這一目標之前,應該先確定哪些方法最重要。這很好,但經過初步探索後,你最好停下來再確認一下目標,然後再達成它們。開發人員非常愛犯只顧低頭拉車,不顧抬頭看路的錯誤。

6.2.4 知道什麼時候停止優化

理論上來說,知道什麼時候停止優化並不難——達成目標之時就是任務完成之日。然而實際中人們很容易陷入性能調優的泥淖。如果事情進展順利,你肯定想要繼續前進並做得更好。而如果不太順利,你為了達成目標就會不斷嘗試新策略。

要想知道什麼時候停止優化,你需要對目標有清醒的認識並理解它們的價值。能達成性能目標的90%通常就足夠了,你還可以利用節省下來的時間去做些別的事。

還要考慮一點,你要看看有多少工作投入到了極少用到的代碼路徑上。通過優化代碼來減少程序運行時長的1%(甚至更少)完全是在浪費時間,但奇怪的是做這種事兒的開發人員數量驚人。

至於該優化什麼,這裡有一組非常簡單的指導規則。你可能需要根據自身情況進行調整,但它們的適用範圍很廣泛:

  • 優化那些重要,而不是最容易的代碼。
  • 首先優化那些最重要(通常是調用最頻繁)的方法。
  • 在遇到那些唾手可得的優化時,把它辦了,但要清楚代碼的調用頻率。

最後再做一輪測量工作。如果還沒達成性能目標,你就需要清查一下,看看離命中目標還有多大差距,以及取得的成績是不是已經對整體性能產生了你所期望的影響。

6.2.5 知道高性能的成本

所有性能調整都貼著價簽。

  • 分析和優化代碼要佔用的時間(在任何軟件項目中,開發人員的時間基本都是最大的開支)。

  • 所做的調整可能會引入額外的技術複雜度(也有簡化代碼的性能優化,但它們不是主流)。

  • 為了讓主處理線程運行得更快,可能會引入額外的線程來執行輔助任務,但這些線程可能會在負載較高時對系統整體產生不可預料的影響。

不管是什麼價簽,你都要重視,並盡量在完成第一輪優化之前找到它們。

這有助於你瞭解提高性能的最大可接受成本。這個成本可能是設定開發人員調優的時間限制,額外的類數或代碼行數。比如說,開發人員決定花在優化上的時間不能超過一個星期,或者因優化而生的類增長不應該超過100%(即大小變成原來的兩倍)。

6.2.6 知道過早優化的危險

關於優化,Donald Knuth有段著名的評論:

程序員浪費了大量時間考慮,或擔心程序中無關緊要部分的速度,並且那些嘗試改進效率的行為實際上有很強的負面影響……過早優化是萬惡之源。 1

1 Donald E. Knuth, 「帶go to語句的結構化編程」, 計算調查,6,no.4(1974年12月)。 http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf。

這段話在業內引起了廣泛爭論,而且人們通常只記住了最後一句。這之所以令人感到遺憾,有如下原因。

  • 在評論的前段,Knuth含蓄地提醒我們要測量,沒有測量就不能確定程序的關鍵部分。

  • 我們再次提醒你,可能不是代碼導致等待時間過長——環境中的其他部分也會產生等待時間。

  • 在完整的評論中,很容易看出Knuth是在談論那些有意識的、齊心協力的優化。

  • 這段評論的簡短版讓它變成了不良設計或糟糕執行選擇的相當巧合的借口。

有些優化體現在良好的編碼風格上:

  • 不要分配不需要的對象。
  • 如果再也不需要調試日誌,就去掉它。

我們在下面的代碼中加了一個檢查,看日誌對象是否處理調試日誌。這種檢查被稱為日誌守衛。如果日誌子系統被設置為不處理調試日誌,這段代碼就不會構造日誌消息,省掉了為了日誌消息而調用currentTimeMillis和構造StringBuilder對象的開銷。

if (log.isDebugEnabled) log.debug(\"Useless log at: \"+
System.currentTimeMillis);
  

但如果調試日誌真的沒有用,我們可以把這段代碼一併去掉,就能再節省兩個處理器週期(日誌守衛的開銷)。

性能調優的工作之一就是從一開始就寫出質地優良、高效運行的代碼。更好地認識Java平台,知道它的底層運行機制(比如理解在合併兩個字符串時隱含的對象分配),並在編碼時考慮到性能問題,才能寫出更好的代碼。

現在我們有了框定性能問題和目標的基本詞彙,還有如何解決問題的方法大綱。但我們還沒解釋為什麼這是軟件工程師會遇到的問題,以及這種需求來自哪裡。要弄懂這個,我們有必要簡單瞭解一下硬件的世界。