[翻译] 拆穿 Java StringBuilder 的谣言

楼主: PsMonkey (痞子军团团长)   2013-04-01 20:11:45
原文网址: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)

Links booklink

Contact Us: admin [ a t ] ucptt.com