为什么在Java 7中,StringBuilder#append(int) 比在Java 8中更快?

11 浏览
0 Comments

为什么在Java 7中,StringBuilder#append(int) 比在Java 8中更快?

在进行有关使用"" + nInteger.toString(int)将整数原始类型转换为字符串的辩论时,我编写了这个JMH微基准测试:little debate

@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机器上使用默认的JMH选项运行了它,该机器上存在两个Java虚拟机(最新版的Mageia 4 64位,Intel i7-3770 CPU,32GB RAM)。第一个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

为什么StringBuilder.append(int)在这个JVM上看起来快得多?查看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文件中AbstractStringBuilderStringBuilderInteger类的源代码,没有找到特别值得注意的内容。除了大量的界面和文档更改外,Integer现在对无符号整数有一些支持,StringBuilder已经稍微重构,以与StringBuffer共享更多的代码。这些变化似乎不会影响StringBuilder#append(int)使用的代码路径,尽管我可能错过了一些东西。

对比IntStr#integerToString()IntStr#stringBuilder0()生成的汇编代码更加有趣。对于这两个JVM生成的IntStr#integerToString()的代码布局基本相似,尽管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源代码相关部分的人应该能回答这些问题...

0