为什么 StringBuilder#append(int) 在 Java 7 中比在 Java 8 中更快?
在调查一些辩论w.r.t.使用和Integer.toString(int)
将整数基元转换为字符串时,我写了这个JMH微基准标记:"" + n
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
protected int counter;
@GenerateMicroBenchmark
public String integerToString() {
return Integer.toString(this.counter++);
}
@GenerateMicroBenchmark
public String stringBuilder0() {
return new StringBuilder().append(this.counter++).toString();
}
@GenerateMicroBenchmark
public String stringBuilder1() {
return new StringBuilder().append("").append(this.counter++).toString();
}
@GenerateMicroBenchmark
public String stringBuilder2() {
return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
}
@GenerateMicroBenchmark
public String stringFormat() {
return String.format("%d", this.counter++);
}
@Setup(Level.Iteration)
public void prepareIteration() {
this.counter = 0;
}
}
我使用默认的JMH选项运行它,其中包含Linux机器上存在的两个Java虚拟机(最新的Mageia 4 64位,Intel i7-3770 CPU,32GB RAM)。第一个JVM是Oracle JDK 8u5 64位提供的JVM:
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)
有了这个JVM,我得到了我所期望的:
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 32317.048 698.703 ops/ms
b.IntStr.stringBuilder0 thrpt 20 28129.499 421.520 ops/ms
b.IntStr.stringBuilder1 thrpt 20 28106.692 1117.958 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20066.939 1052.937 ops/ms
b.IntStr.stringFormat thrpt 20 2346.452 37.422 ops/ms
即,由于创建对象和附加空字符串的额外开销,因此使用类的速度较慢。使用速度甚至慢一个数量级左右。StringBuilder
StringBuilder
String.format(String, ...)
另一方面,发行版提供的编译器基于OpenJDK 1.7:
java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
这里的结果很有趣:
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 31249.306 881.125 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39486.857 663.766 ops/ms
b.IntStr.stringBuilder1 thrpt 20 41072.058 484.353 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20513.913 466.130 ops/ms
b.IntStr.stringFormat thrpt 20 2068.471 44.964 ops/ms
为什么这个JVM出现得更快?查看类源代码,发现没有什么特别有趣的 - 所讨论的方法几乎与.有趣的是,附加结果(微基准标记)似乎并不快。StringBuilder.append(int)
StringBuilder
Integer#toString(int)
Integer.toString(int)
stringBuilder2
这种性能差异是否是测试工具的问题?或者我的OpenJDK JVM是否包含会影响此特定代码(反)模式的优化?
编辑:
为了进行更直接的比较,我安装了Oracle JDK 1.7u55:
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)
结果与OpenJDK的结果相似:
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 32502.493 501.928 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39592.174 428.967 ops/ms
b.IntStr.stringBuilder1 thrpt 20 40978.633 544.236 ops/ms
这似乎是一个更普遍的Java 7与Java 8的问题。也许Java 7有更激进的字符串优化?
编辑2:
为完整起见,以下是这两个 JVM 的字符串相关 VM 选项:
对于 Oracle JDK 8u5:
$ /usr/java/default/bin/java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}
对于 OpenJDK 1.7:
$ java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}
bool UseStringCache = false {product}
该选项在Java 8中被删除,没有替换,所以我怀疑这有什么区别。其余选项似乎具有相同的设置。UseStringCache
编辑3:
并排比较 的源代码和 文件中的类,没有发现任何值得注意的结果。除了大量的修饰和文档更改之外,现在还对无符号整数提供了一些支持,并且已经过略微重构,以便与 共享更多代码。这些更改似乎都不会影响 所使用的代码路径,尽管我可能遗漏了一些东西。AbstractStringBuilder
StringBuilder
Integer
src.zip
Integer
StringBuilder
StringBuffer
StringBuilder#append(int)
比较为 生成的程序集代码,并且更有趣。生成的代码的基本布局对于两个JVM都是相似的,尽管Oracle JDK 8u5似乎更积极地在代码中内联一些调用。与Java源代码有明确的对应关系,即使对于汇编经验最少的人来说也是如此。IntStr#integerToString()
IntStr#stringBuilder0()
IntStr#integerToString()
Integer#toString(int)
然而,的汇编代码是完全不同的。Oracle JDK 8u5生成的代码再次与Java源代码直接相关 - 我可以很容易地识别出相同的布局。相反,OpenJDK 7生成的代码对于未经训练的眼睛(就像我的眼睛一样)几乎是无法识别的。调用似乎已被删除,在构造函数中创建数组也是如此。此外,反汇编器插件无法像在JDK 8中那样提供对源代码的引用。IntStr#stringBuilder0()
new StringBuilder()
StringBuilder
我假设这要么是OpenJDK 7中更积极的优化过程的结果,要么是为某些操作插入手写的低级代码的结果。我不确定为什么在我的JVM 8实现中没有发生这种优化,或者为什么在JVM 7中没有实现相同的优化。我猜熟悉JRE源代码相关部分的人必须回答这些问题......StringBuilder
Integer#toString(int)