为什么StringBuilder#在Java 7中追加(int)比在Java 8中更快?
虽然使用"" + n和Integer.toString(int)将一个整数原语转换为一个string调查了一点争论,我写了这个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; } } 
我用Linux机器上的两个Java虚拟机(最新的Mageia 4 64位,Intel i7-3770 CPU,32GB RAM)的默认JMH选项运行它。 第一个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较慢,因为创buildStringBuilder对象和附加一个空string的额外开销。 使用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 microbenchmark)似乎并不快。 
这种性能差异是否与testing工具有关? 或者我的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有更积极的string优化?
编辑2 :
为了完整起见,以下是这两个JVM的与string相关的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选项已经被删除,没有replace,所以我怀疑这是否UseStringCache 。 其余选项似乎具有相同的设置。 
编辑3:
 来自src.zip文件的AbstractStringBuilder , StringBuilder和Integer类的源代码的并行比较没有显示任何noteworty。  Integer现在对unsigned整数有一些支持,而且StringBuilder已被稍微重构,以便与StringBuffer共享更多的代码。 这些改变都没有影响StringBuilder#append(int)使用的代码path,尽pipe我可能错过了一些东西。 
 为IntStr#integerToString()和IntStr#stringBuilder0()生成的汇编代码的比较更加有趣。 对于两个JVM, IntStr#integerToString()生成的代码的基本布局是相似的,尽pipeOracle JDK 8u5似乎在Integer#toString(int)代码中内联了一些调用。 与Java源代码有明确的对应关系,即使是对于组装经验最less的人也是如此。 
 然而, IntStr#stringBuilder0()的汇编代码是完全不同的。  Oracle JDK 8u5生成的代码再次与Java源代码直接相关 – 我可以轻松识别相同的布局。 相反,OpenJDK 7生成的代码几乎无法识别未经训练的眼睛(如我的)。  new StringBuilder()调用看起来已经被移除了,就像在StringBuilder构造函数中创build数组一样。 另外,反汇编程序插件不能像在JDK 8中那样提供对源代码的引用。 
 我认为这可能是OpenJDK 7中更积极的优化传递的结果,或者更可能是为某些StringBuilder操作插入手写低级代码的结果。 我不确定为什么这个优化不会发生在我的JVM 8实现中,或者为什么没有在JVM 7中为Integer#toString(int)实现相同的优化。我想有人熟悉JRE源代码的相关部分将不得不回答这些问题… 
  TL; DR: append副作用明显破坏了StringConcat的优化。 
非常好的分析在原来的问题和更新!
为了完整性,下面是几个缺失的步骤:
- 
通过 -XX:+PrintInlining查看7u55和8u5。 在7月55日,你会看到这样的事情:@ 16 org.sample.IntStr::inlineSideEffect (25 bytes) force inline by CompilerOracle @ 4 java.lang.StringBuilder::<init> (7 bytes) inline (hot) @ 18 java.lang.StringBuilder::append (8 bytes) already compiled into a big method @ 21 java.lang.StringBuilder::toString (17 bytes) inline (hot)…在8u5: @ 16 org.sample.IntStr::inlineSideEffect (25 bytes) force inline by CompilerOracle @ 4 java.lang.StringBuilder::<init> (7 bytes) inline (hot) @ 3 java.lang.AbstractStringBuilder::<init> (12 bytes) inline (hot) @ 1 java.lang.Object::<init> (1 bytes) inline (hot) @ 18 java.lang.StringBuilder::append (8 bytes) inline (hot) @ 2 java.lang.AbstractStringBuilder::append (62 bytes) already compiled into a big method @ 21 java.lang.StringBuilder::toString (17 bytes) inline (hot) @ 13 java.lang.String::<init> (62 bytes) inline (hot) @ 1 java.lang.Object::<init> (1 bytes) inline (hot) @ 55 java.util.Arrays::copyOfRange (63 bytes) inline (hot) @ 54 java.lang.Math::min (11 bytes) (intrinsic) @ 57 java.lang.System::arraycopy (0 bytes) (intrinsic)你可能注意到7u55的版本比较浅,在 StringBuilder方法之后看起来没有任何东西被调用 – 这是string优化生效的一个很好的指示。 事实上,如果你使用-XX:-OptimizeStringConcat-OptimizeStringConcat运行7u55,那么subcalls将会重新出现,性能会下降到8u5的水平。
- 
好的,所以我们需要弄清楚为什么8u5不能做同样的优化。 Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot为“StringBuilder”找出哪里虚拟机处理StringConcat优化;; 这会让你进入 src/share/vm/opto/stringopts.cpp
- 
hg log src/share/vm/opto/stringopts.cpp找出那里的最新变化。 其中一名候选人是:changeset: 5493:90abdd727e64 user: iveresov date: Wed Oct 16 11:13:15 2013 -0700 summary: 8009303: Tiered: incorrect results in VM tests stringconcat...
- 
在OpenJDK邮件列表上查找审查主题(对于变更摘要来说,很容易google): http : //mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html 
- 
Spot“string连接优化优化将模式折叠为一个string的单个分配并直接生成结果。优化代码中可能发生的所有可能的错误从头重新开始(从StringBuffer分配开始) 这意味着整个模式必须是我自由的副作用。 “尤里卡? 
- 
写出对比的基准: @Fork(5) @Warmup(iterations = 5) @Measurement(iterations = 5) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) public class IntStr { private int counter; @GenerateMicroBenchmark public String inlineSideEffect() { return new StringBuilder().append(counter++).toString(); } @GenerateMicroBenchmark public String spliceSideEffect() { int cnt = counter++; return new StringBuilder().append(cnt).toString(); } }
- 
在JDK 7u55上测量,看到内联/拼接副作用的相同性能: Benchmark Mode Samples Mean Mean error Units osIntStr.inlineSideEffect avgt 25 65.460 1.747 ns/op osIntStr.spliceSideEffect avgt 25 64.414 1.323 ns/op
- 
在JDK 8u5上测量,看到内嵌效果的性能下降: Benchmark Mode Samples Mean Mean error Units osIntStr.inlineSideEffect avgt 25 84.953 2.274 ns/op osIntStr.spliceSideEffect avgt 25 65.386 1.194 ns/op
- 
提交bug报告( https://bugs.openjdk.java.net/browse/JDK-8043677 )与VM伙伴讨论这个行为。 原始修复的基本原理是坚如磐石的,但是如果我们能够/应该在这样的一些微不足道的情况下得到这个优化,这将是非常有趣的。 
- 
??? 
- 
利润。 
 是的,我应该发布基准的结果,从而增加了从StringBuilder链增加,在整个链之前。 另外,切换到平均时间,ns / op。 这是JDK 7u55: 
Benchmark Mode Samples Mean Mean error Units osIntStr.integerToString avgt 25 153.805 1.093 ns/op osIntStr.stringBuilder0 avgt 25 128.284 6.797 ns/op osIntStr.stringBuilder1 avgt 25 131.524 3.116 ns/op osIntStr.stringBuilder2 avgt 25 254.384 9.204 ns/op osIntStr.stringFormat avgt 25 2302.501 103.032 ns/op
这是8u5:
Benchmark Mode Samples Mean Mean error Units osIntStr.integerToString avgt 25 153.032 3.295 ns/op osIntStr.stringBuilder0 avgt 25 127.796 1.158 ns/op osIntStr.stringBuilder1 avgt 25 131.585 1.137 ns/op osIntStr.stringBuilder2 avgt 25 250.980 2.773 ns/op osIntStr.stringFormat avgt 25 2123.706 25.105 ns/op
 在8u5中, stringFormat实际上有点快,而其他所有的testing都是一样的。 这巩固了原始问题中罪魁祸首中SB链副作用破坏的假设。 
 我认为这与CompileThreshold标志有关,该标志控制JIT将字节码编译成机器码的时间。 
Oracle JDK在http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html上的默认计数为10,000。;
OpenJDK在哪里我找不到这个标志上的最新文件; 但是一些邮件线程提示的门槛要低得多: http : //mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html
 此外,请尝试打开/closuresOracle JDK标志,如-XX:+UseCompressedStrings和-XX:+OptimizeStringConcat 。 我不确定这些标志是否在OpenJDK上默认打开。 有人可以请build议。 
你可以做的一个经验就是首先运行程序很多次,比如说3万个循环,做一个System.gc(),然后试着看性能。 我相信他们会产生同样的结果。
我假设你的GC设置也是一样的。 否则,你正在分配很多的对象,GC可能是你的运行时间的主要部分。