在探讨将整数原始类型转换为字符串时,我写了这个JMH微基准测试,目的是对使用"" + n
和Integer.toString(int)
进行一次小辩论:
@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;
}
}
我在我的Linux机器上(最新的Mageia 4 64位,Intel i7-3770 CPU,32GB RAM)使用默认的JMH选项运行了它,并且使用了两个Java虚拟机。第一个JVM是Oracle JDK 8u5 64位版本提供的。
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}
UseStringCache
选项在Java 8中被删除,没有替代品,因此我怀疑这不会有任何影响。其余选项似乎具有相同的设置。
编辑3:
从src.zip
文件中比较AbstractStringBuilder
、StringBuilder
和Integer
类的源代码,没有发现任何值得注意的东西。除了大量的美化和文档更改外,Integer
现在对无符号整数有一些支持,StringBuilder
已经稍微重构,以与StringBuffer
共享更多的代码。这些变化似乎都不会影响StringBuilder#append(int)
使用的代码路径,但我可能错过了什么。
IntStr#integerToString()
和 IntStr#stringBuilder0()
生成的汇编代码更有趣。对于 IntStr#integerToString()
生成的代码布局在两个JVM中都很相似,尽管 Oracle JDK 8u5 在内联 Integer#toString(int)
代码中的一些调用方面似乎更为激进。即使对于具有最少汇编经验的人来说,Java源代码也有明显的对应关系。
然而,IntStr#stringBuilder0()
的汇编代码却截然不同。由Oracle JDK 8u5生成的代码再次直接与Java源代码相关 - 我可以轻松地识别出相同的布局。相反,由OpenJDK 7生成的代码对于未经训练的眼睛(如我)几乎无法识别。看起来似乎删除了 new StringBuilder()
调用以及在 StringBuilder
构造函数中创建数组。此外,反汇编插件无法像JDK 8那样提供与源代码的许多引用。
我认为这可能是OpenJDK 7中更为激进的优化过程的结果,或者更可能是插入手写低级代码处理某些StringBuilder
操作的结果。我不确定为什么在我的JVM 8实现中没有发生此优化,或者为什么在JVM 7中没有为Integer#toString(int)
实现相同的优化。我猜想熟悉JRE源代码相关部分的人应该回答这些问题...
new StringBuilder().append(this.counter++).toString();
,还有一个用途为return "" + this.counter++
的第三个测试? - assyliasString.format("%d",n)
比其他所有东西慢一个数量级... - thkala