thk*_*ala 76 java performance java-7 java-8 jmh
在研究使用和将整数原语转换为字符串的一些小辩论时"" + 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;
}
}
Run Code Online (Sandbox Code Playgroud)
我使用默认的JMH选项运行它,我的Linux机器上都存在两个Java VM(最新的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)
Run Code Online (Sandbox Code Playgroud)
有了这个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
Run Code Online (Sandbox Code Playgroud)
即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)
Run Code Online (Sandbox Code Playgroud)
这里的结果很有趣:
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
Run Code Online (Sandbox Code Playgroud)
为什么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)
Run Code Online (Sandbox Code Playgroud)
结果与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
Run Code Online (Sandbox Code Playgroud)
看起来这是一个更普遍的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}
Run Code Online (Sandbox Code Playgroud)
对于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}
Run Code Online (Sandbox Code Playgroud)
该UseStringCache选项在Java 8中删除,没有替换,所以我怀疑这有什么不同.其余选项似乎具有相同的设置.
编辑3:
甲侧方比较的源代码AbstractStringBuilder,StringBuilder和Integer类从src.zip文件揭示了什么noteworty.除了大量的修饰和文档更改之外,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实现中没有发生这种优化,或者为什么Integer#toString(int)在JVM 7 中没有实现相同的优化.我想有人熟悉JRE源代码的相关部分将不得不回答这些问题......
Ale*_*lev 95
TL; DR:append明显破坏StringConcat优化的副作用.
在原始问题和更新中非常好的分析!
为了完整起见,以下是一些缺失的步骤:
查看-XX:+PrintInlining7u55和8u5.在7u55,您将看到如下内容:
Run Code Online (Sandbox Code Playgroud)@ 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:
Run Code Online (Sandbox Code Playgroud)@ 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方法之后没有调用任何内容- 这是字符串优化有效的良好指示.实际上,如果你运行7u55 -XX:-OptimizeStringConcat,子弹将重新出现,性能下降到8u5级别.
好的,所以我们需要弄清楚为什么8u5没有做同样的优化.Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot为"StringBuilder"找出VM处理StringConcat优化的位置; 这会让你进入src/share/vm/opto/stringopts.cpp
hg log src/share/vm/opto/stringopts.cpp弄清楚那里的最新变化.其中一位候选人将是:
Run Code Online (Sandbox Code Playgroud)changeset: 5493:90abdd727e64 user: iveresov date: Wed Oct 16 11:13:15 2013 -0700 summary: 8009303: Tiered: incorrect results in VM tests stringconcat...
在OpenJDK邮件列表上查找评论主题(谷歌的变更集摘要很容易):http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html
Spot"String concat优化优化将模式[...]折叠为字符串的单个分配并直接形成结果.优化代码中可能发生的所有可能的deopts从头开始重新启动此模式(从StringBuffer分配开始) .这意味着整个模式必须我没有副作用. "尤里卡?
写出对比鲜明的基准:
Run Code Online (Sandbox Code Playgroud)@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上进行测量,看到内联/拼接副作用的性能相同:
Run Code Online (Sandbox Code Playgroud)Benchmark Mode Samples Mean Mean error Units o.s.IntStr.inlineSideEffect avgt 25 65.460 1.747 ns/op o.s.IntStr.spliceSideEffect avgt 25 64.414 1.323 ns/op
在JDK 8u5上测量它,看到性能下降与内联效果:
Run Code Online (Sandbox Code Playgroud)Benchmark Mode Samples Mean Mean error Units o.s.IntStr.inlineSideEffect avgt 25 84.953 2.274 ns/op o.s.IntStr.spliceSideEffect avgt 25 65.386 1.194 ns/op
提交错误报告(https://bugs.openjdk.java.net/browse/JDK-8043677)与VM人员讨论此行为.原始修复的基本原理是坚如磐石的,但有趣的是,如果我们能够/应该在这些一些微不足道的案例中找回这种优化.
???
利润.
是的,我应该发布基准的结果,从StringBuilder链中移动增量,在整个链之前完成.此外,切换到平均时间和ns/op.这是JDK 7u55:
Run Code Online (Sandbox Code Playgroud)Benchmark Mode Samples Mean Mean error Units o.s.IntStr.integerToString avgt 25 153.805 1.093 ns/op o.s.IntStr.stringBuilder0 avgt 25 128.284 6.797 ns/op o.s.IntStr.stringBuilder1 avgt 25 131.524 3.116 ns/op o.s.IntStr.stringBuilder2 avgt 25 254.384 9.204 ns/op o.s.IntStr.stringFormat avgt 25 2302.501 103.032 ns/op
这是8u5:
Run Code Online (Sandbox Code Playgroud)Benchmark Mode Samples Mean Mean error Units o.s.IntStr.integerToString avgt 25 153.032 3.295 ns/op o.s.IntStr.stringBuilder0 avgt 25 127.796 1.158 ns/op o.s.IntStr.stringBuilder1 avgt 25 131.585 1.137 ns/op o.s.IntStr.stringBuilder2 avgt 25 250.980 2.773 ns/op o.s.IntStr.stringFormat avgt 25 2123.706 25.105 ns/op
stringFormat在8u5中实际上要快一点,所有其他测试都是一样的.这巩固了假设在原始问题的主要罪魁祸首SB链中的副作用破坏.
我认为这与CompileThreshold控制何时通过JIT将字节代码编译成机器代码的标志有关.
Oracle JDK的默认计数为10,000,文档位于http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.
哪里有OpenJDK我找不到关于这面旗帜的最新文件; 但是一些邮件线程提出了一个更低的门槛:http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html
另外,尝试打开/关闭Oracle JDK标志,如-XX:+UseCompressedStrings和-XX:+OptimizeStringConcat.我不确定这些标志是否在OpenJDK上默认打开.有人可以建议.
你可以做的一个经验是首先运行程序很多次,比如30,000个循环,做一个System.gc(),然后尝试查看性能.我相信他们会产生同样的结果.
我认为你的GC设置也是一样的.否则,您将分配大量对象,GC可能是您运行时间的主要部分.
| 归档时间: |
|
| 查看次数: |
9972 次 |
| 最近记录: |