此影响是由类型配置文件污染引起的。让我解释一个简化的基准:
@State(Scope.Benchmark)public class Streams { @Param({"500", "520"}) int iterations; @Setup public void init() { for (int i = 0; i < iterations; i++) { Stream.empty().reduce((x, y) -> x); } } @Benchmark public long loop() { return Stream.empty().count(); }}
尽管
iteration此处的参数变化很小并且不会影响主基准循环,但结果却暴露出令人惊讶的2.5倍性能下降:
Benchmark (iterations) Mode Cnt Score Error UnitsStreams.loop500 thrpt 5 29491,039 ± 240,953 ops/msStreams.loop520 thrpt 5 11867,860 ± 344,779 ops/ms
现在让我们运行带有
-prof perfasm选项的JMH 来查看最热门的代码区域:
快速案例(迭代次数= 500):
....[Hottest Methods (after inlining)].................................. 48,66% bench.generated.Streams_loop::loop_thrpt_jmhStub 23,14% <unknown> 2,99% java.util.stream.Sink$ChainedReference::<init> 1,98% org.openjdk.jmh.infra.Blackhole::consume 1,68% java.util.Objects::requireNonNull 0,65% java.util.stream.AbstractPipeline::evaluate
慢速情况(迭代次数= 520):
....[Hottest Methods (after inlining)].................................. 40,09% java.util.stream.ReduceOps$ReduceOp::evaluateSequential 22,02% <unknown> 17,61% bench.generated.Streams_loop::loop_thrpt_jmhStub 1,25% org.openjdk.jmh.infra.Blackhole::consume 0,74% java.util.stream.AbstractPipeline::evaluate
看起来情况很慢
ReduceOp.evaluateSequential,在非内联方法中花费的时间最多。此外,如果我们研究此方法的汇编代码,则会发现最长的 *** 作是
checkcast。
您知道HotSpot编译器的工作原理:在JIT启动之前,将在解释器中执行一个方法一段时间,以收集配置文件数据,例如,调用了哪些方法,看到了哪些类,采用了哪些分支等。也以C1编译的代码收集。该配置文件然后用于生成C2优化的代码。但是,如果应用程序在中间更改执行模式,则生成的代码对于修改后的行为可能不是最佳的。
让我们使用
-XX:+PrintMethodData(在调试JVM中可用)比较执行配置文件:
----- Fast case -----java.util.stream.ReduceOps$ReduceOp::evaluateSequential(Ljava/util/stream/PipelineHelper;Ljava/util/Spliterator;)Ljava/lang/Object; interpreter_invocation_count: 13382 invocation_counter: 13382 backedge_counter: 0 mdo size: 552 bytes0 aload_11 fast_aload_02 invokevirtual 3 <java/util/stream/ReduceOps$ReduceOp.makeSink()Ljava/util/stream/ReduceOps$AccumulatingSink;> 0 bci: 2 VirtualCallData count(0) entries(1) 'java/util/stream/ReduceOps'(12870 1.00)5 aload_26 invokevirtual 4 <java/util/stream/PipelineHelper.wrapAndCopyInto(Ljava/util/stream/Sink;Ljava/util/Spliterator;)Ljava/util/stream/Sink;> 48 bci: 6 VirtualCallData count(0) entries(1) 'java/util/stream/ReferencePipeline'(12870 1.00)9 checkcast 5 <java/util/stream/ReduceOps$AccumulatingSink> 96 bci: 9 ReceiverTypeData count(0) entries(1) 'java/util/stream/ReduceOpsReducingSink'(12870 1.00)12 invokeinterface 6 <java/util/stream/ReduceOps$AccumulatingSink.get()Ljava/lang/Object;> 144 bci: 12 VirtualCallData count(0) entries(1) 'java/util/stream/ReduceOpsReducingSink'(12870 1.00)17 areturn----- Slow case -----java.util.stream.ReduceOps$ReduceOp::evaluateSequential(Ljava/util/stream/PipelineHelper;Ljava/util/Spliterator;)Ljava/lang/Object; interpreter_invocation_count: 54751 invocation_counter: 54751 backedge_counter: 0 mdo size: 552 bytes0 aload_11 fast_aload_02 invokevirtual 3 <java/util/stream/ReduceOps$ReduceOp.makeSink()Ljava/util/stream/ReduceOps$AccumulatingSink;> 0 bci: 2 VirtualCallData count(0) entries(2) 'java/util/stream/ReduceOps'(16 0.00) 'java/util/stream/ReduceOps'(54223 1.00)5 aload_26 invokevirtual 4 <java/util/stream/PipelineHelper.wrapAndCopyInto(Ljava/util/stream/Sink;Ljava/util/Spliterator;)Ljava/util/stream/Sink;> 48 bci: 6 VirtualCallData count(0) entries(2) 'java/util/stream/ReferencePipeline$Head'(16 0.00) 'java/util/stream/ReferencePipeline'(54223 1.00)9 checkcast 5 <java/util/stream/ReduceOps$AccumulatingSink> 96 bci: 9 ReceiverTypeData count(0) entries(2) 'java/util/stream/ReduceOpsReducingSink'(16 0.00) 'java/util/stream/ReduceOpsReducingSink'(54228 1.00)12 invokeinterface 6 <java/util/stream/ReduceOps$AccumulatingSink.get()Ljava/lang/Object;> 144 bci: 12 VirtualCallData count(0) entries(2) 'java/util/stream/ReduceOpsReducingSink'(16 0.00) 'java/util/stream/ReduceOpsReducingSink'(54228 1.00)17 areturn
您会看到,初始化循环耗时太长,以致其统计信息无法显示在执行配置文件中:所有虚拟方法都有两个实现,而checkcast也有两个不同的条目。在最快的情况下,概要文件不会受到污染:所有站点都是单态的,JIT可以轻松地内联和优化它们。
原始基准测试也是如此:
init()方法中的较长流 *** 作污染了配置文件。如果您使用概要文件和分层编译选项,则结果可能会大不相同。例如,尝试
-XX:-ProfileInterpreter
-XX:Tier3InvocationThreshold=1000
-XX:-TieredCompilation
最后,这个问题不是唯一的。由于配置文件污染,已经存在与性能降低相关的多个JVM错误:JDK-8015416,JDK-8015417,JDK-8059879
…希望在Java 9中会有所改善。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)