原文網址: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)