看板 Translate-CS 關於我們 聯絡資訊
原文網址:http://skuro.tk/2013/03/11/java-stringbuilder-myth-now-with-content/ 譯文網址:http://blog.dontcareabout.us/2013/04/java-stringbuilder.html BBS 版以 markdown 語法撰寫 喔對,這篇真的不是愚人節活動 update: 不,這篇真的好像被愚人到了 詳情請參閱 java 版的 #1HMQB1It 不過這篇就... 還是放著吧...... [死] ______________________________________________________________________ 謠言...... ========== > 用 + 號來連接兩個字串是萬惡的根源。 —— 不知名的 Java 開發人員 **註:**這裡討論用到的程式碼都可以在 [Github] 上找到。 在大學的時候,我學到在 Java 中用 + 號來連接字串是一種致命的效能罪惡。 最近在 [Backbase R&D] 有一個內部的 review, 這個 recurring mantra 變成了謠言, 因為當你使用 + 號來連接字串時,`javac` 會在底層使用 `StringBuilder`。 我要證明這件事情,並驗證在不同環境下的真實性。 [Github]: https://github.com/skuro/stringbuilder [Backbase R&D]: http://www.backbase.com/ 測試...... ========== 倚賴 compiler 對連接字串這件事作最佳化, 這意味著使用不同的 JDK 可能會得到完全不一樣的結果。 就我平常的工作環境,我考慮這三個 JDK 供應商: * Oracle JDK * IBM JDK * ECJ(僅針對開發人員) 此外,雖然我們官方支援 Java 5 跟 Java 6, 不過我們也在研究讓產品可以支援到 Java 7, adding another three-folded level of indirection on top of the three vendors.(譯註:翻譯不能 Orz) 為了<strike>懶惰</strike>簡單起見, `ecj` compile 出來的 bytecode 只會在 Oracle JDK 7 上頭執行。 我準備了一個 [VirtualBox] VM 安裝上述所有的 JDK, 然後我寫了一些 class 來代表三種不同的字串連接方式, 每一個 method 會有三到四個連接字串的動作、取決於 test case。 [VirtualBox]: https://www.virtualbox.org/ 這些 test case 在每一回合會執行一千次、總共 100 回合。 同一個 test 的所有回合都會在同一個 VM 上頭執行, 在跑不同 test case 的時候重開 VM, 這都是為了讓 Java 在執行期可以作任何可能的最佳化動作、 而不會影響到其他 test case。 所有 JVM 啟動時都用預設的設定。 更細節的部份可以參考 benchmark runner [script]。 [script]: https://github.com/skuro/stringbuilder/blob/master/bench.sh 程式碼 ====== 所有 test case 以及 test suite 的完整程式碼都在 [Github] 上頭。 下面這幾個不同的 test case 用來測量 + 號與直接使用 `StringBuilder` 連接字串的效能差異: // String concat with plus String result = "const1" + base; result = result + "const2"; ______________________________________________________________________ // String concat with a StringBuilder new StringBuilder() .append("const1") .append(base) .append("const2") .append(append) .toString(); } ______________________________________________________________________ // String concat with a StringBuilder new StringBuilder("const1") .append(base) .append("const2") .append(append) .toString(); } 大體上的想法是在常數字串前後都連接一個變數。 最後兩個 case 都是用 `StringBuilder`, 差異是後頭使用了傳入一個參數的 constructor, 在 builder 初始化的時候就初始化結果的一部分。 結果...... ========= 前提講的差不多了,下面這些產生出來的圖表, 每一個點對應到單一個測試回合(對同一個測試執行 1000 次)。 後頭會討這些結果以及一些有趣的細節。 ![plus](http://skuro.tk/img/post/catplus.png) ![StringBuffer()](http://skuro.tk/img/post/catsb.png) ![StringBuffer(String)](http://skuro.tk/img/post/catsb2.png) 討論...... ========== Oracle JDK 5 輸的很徹底,跟其他相比就是個 B 咖。 但那不是這次要討論的範圍,所以暫時不理會吧。 在上頭的圖表當中我觀察到兩件有趣的事情。 首先,使用 + 號跟明確指定用 `StringBuilder` 的確存在普遍的落差, *特別是*在使用 Oracle Java 5 的時候比其他人差了三倍。 第二個觀察到的現象是,大多數的 JDK 在明確指定用 `StringBuilder 時可以提供比 + 號快兩倍的速度, 而 **IBM JDK 6 看起來沒有減損任何效能**, 在所有的 test case 中始終保持在 25ms 左右的時間。 仔細看一下產生出來的 bytecode 揭露了一些有趣的細節。 bytecode 表示: ============== **註:**[Github] 上也有 decompile 後的 class。 在所有的 JDK 上應該**總是**用 `StringBuilder` 來實作連接字串, 即使有 + 可以用。 此外,比較所有供應商的所有版本, 在同樣的 test case 下**幾乎沒有什麼分別**。 唯一比較有區隔的是 [ecj],它是唯一一個對 `CatPlus` test case 作最佳化, 會使用傳入一個參數的 `StringBuilder` constructor,而不是 `StringBuilder()`。 [ecj]: https://github.com/skuro/stringbuilder/blob/master/ecj/CatPlus.class.txt 比較產生的 bytecode 可以看到在不同情境下可能會影響效能的部份: * 用 + 號連接字串時,每一次都會建立一個**新的** `StringBuilder` **instance**。 這很容易導致效能下降,因為要產生一堆用完就丟 instance, 而造成 garbage collector 的壓力。 * compiler 會依照字面上的意思, 只有在你指定用傳入一個參數的 `StringBuilder` constructor, compiler 才會用它。 這分別導致 [CatSB] 呼叫了四次 `StringBuilder.append()`、 而 [CatSB2] 呼叫三次。 [CatSB]: https://github.com/skuro/stringbuilder/blob/master/ecj/CatSB.class.txt [CatSb2]: https://github.com/skuro/stringbuilder/blob/master/ecj/CatSB2.class.txt 結論...... ========= 分析 bytecode 提供了問題的最終答案: > 需要明確指定用 `StringBuilder` 來增進效能嗎? > **是的!** 上面的圖表顯示的很清楚了,用 + 號會損失 50% 的效能; 除非你用 IBM JDK 6, 那只會筆明確指定使用 `StringBuilder` 稍微差一點點。 此外,看 *JIT 最佳化* 如何影響整體效能十分有趣。 例如:即使兩個指定使用 `StringBuilder` 的 test case, 它們的 bytecode 看起來不一樣, 但是長時間運作之後它們得到的結果還是幾乎一樣的。 ![confirmed](http://skuro.tk/img/post/myth-confirmed.jpg) -- 錢鍾書: 說出來的話 http://www.psmonkey.org 比不上不說出來的話 Java 版 cookcomic 版 只影射著說不出來的話 and more...... -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 114.25.15.134 修正一些 typo ※ 編輯: PsMonkey 來自: 114.25.15.134 (04/01 22:14) ※ 編輯: PsMonkey 來自: 114.25.15.134 (04/01 23:10)