在研究使用 "" + n
和 Integer.toString(int)
将整数原语转换为字符串时,我编写了这个 JMH微基准:
@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 选项运行它,这两个 Java VM 都存在于我的 Linux 机器上(最新的 Mageia 464位,Intel i7-3770 CPU,32 GB RAM)。第一个 JVM 是由 Oracle JDK 提供的 8u564位:
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
对象和附加空字符串会带来额外的开销。使用 ABc2的速度更慢,大约慢了一个数量级。
另一方面,发行版提供的编译器基于 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
微基准测试)的结果似乎并不快。
这种性能差异是测试装具的问题吗?还是我的 OpenJDKJVM 包含会影响这个特定代码(反)模式的优化?
编辑:
为了进行更直接的比较,我安装了 Oracle JDK 1.7 u55:
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
这似乎是一个更普遍的 Java7与 Java8问题。也许 Java7有更积极的字符串优化?
编辑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}
在 Java8中删除了 UseStringCache
选项,没有替换,所以我怀疑这有什么不同。其余的选项似乎具有相同的设置。
编辑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 源代码直接相关——我可以很容易地识别出相同的布局。相反,由 OpenJDK7生成的代码几乎无法被未经训练的眼睛识别(就像我的眼睛一样)。new StringBuilder()
调用似乎被删除了,在 StringBuilder
构造函数中创建的数组也被删除了。此外,反汇编程序插件不能像 JDK 8中那样提供对源代码的引用。
我假设这要么是 OpenJDK7中更积极的优化过程的结果,要么更可能是为某些 StringBuilder
操作插入手写的低级代码的结果。我不确定为什么在我的 JVM 8实现中没有发生这种优化,或者为什么在 JVM 7中没有对 Integer#toString(int)
实现相同的优化。我想熟悉 JRE 源代码相关部分的人必须回答这些问题..。