Dmi*_*kiy 3 java performance lambda microbenchmark
我最近在分析我的代码并发现了一个有趣的瓶颈.这是基准:
@BenchmarkMode(Mode.Throughput)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
public class Contains {
private int[] ar = new int[] {1,2,3,4,5,6,7};
private int val = 5;
@Benchmark
public boolean naive() {
return contains(ar, val);
}
@Benchmark
public boolean lambdaArrayStreamContains() {
return Arrays.stream(ar).anyMatch(i -> i == val);
}
@Benchmark
public boolean lambdaIntStreamContains() {
return IntStream.of(ar).anyMatch(i -> i == val);
}
private static boolean contains(int[] ar, int value) {
for (int arVal : ar) {
if (arVal == value) {
return true;
}
}
return false;
}
}
Run Code Online (Sandbox Code Playgroud)
结果:
Benchmark Mode Cnt Score Error Units
Contains.lambdaArrayStreamContains thrpt 10 22867.962 ± 1049.649 ops/s
Contains.lambdaIntStreamContains thrpt 10 22983.800 ± 593.580 ops/s
Contains.naive thrpt 10 228002.406 ± 8591.186 ops/s
Run Code Online (Sandbox Code Playgroud)
如果显示Array包含通过lambda的操作比使用简单循环的naive实现慢10倍.我知道lambdas应该慢一点.但是10次?我做错了lambda还是这是java的一些问题?
您的基准测试实际上并不衡量anyMatch性能,而是流量开销.与诸如五元素阵列查找之类的非常简单的操作相比,这种开销可能显得很大.
如果我们从相对绝对数字开始,那么放缓将不会那么可怕.让我们测量延迟而不是吞吐量,以获得更清晰的图像.我省略了lambdaIntStream基准测试,因为它的工作方式完全相同lambdaArrayStream.
Benchmark Mode Cnt Score Error Units
Contains.lambdaArrayStream avgt 5 53,242 ± 2,034 ns/op
Contains.naive avgt 5 5,876 ± 0,404 ns/op
Run Code Online (Sandbox Code Playgroud)
5.8 ns大约是2.4 GHz CPU的14个周期.工作量非常小,任何额外的周期都会很明显.那么流操作的开销是多少?
现在用-prof gcprofiler 重新运行基准测试.它将显示堆分配的数量:
Benchmark Mode Cnt Score Error Units
Contains.lambdaArrayStream:·gc.alloc.rate.norm avgt 5 152,000 ± 0,001 B/op
Contains.naive:·gc.alloc.rate.norm avgt 5 ? 10?? B/op
Run Code Online (Sandbox Code Playgroud)
lambdaArrayStream每次迭代分配152个字节,而naive基准分配什么都没有.当然,分配不是免费的:至少有5个对象构造成支持anyMatch,每个对象需要几纳秒:
i -> i == valjava.util.stream实现有点复杂,因为它必须支持流源,中间和终端操作的所有组合.如果你看一下anyMatch你的基准测试中的调用堆栈,你会看到类似的东西:
at bench.Contains.lambda$lambdaArrayStream$0(Contains.java:24)
at java.util.stream.MatchOps$2MatchSink.accept(MatchOps.java:119)
at java.util.Spliterators$IntArraySpliterator.tryAdvance(Spliterators.java:1041)
at java.util.stream.IntPipeline.forEachWithCancel(IntPipeline.java:162)
at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:230)
at java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:196)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.IntPipeline.anyMatch(IntPipeline.java:477)
at bench.Contains.lambdaArrayStream(Contains.java:23)
Run Code Online (Sandbox Code Playgroud)
并非所有这些方法调用都可以内联.此外,JVM限制内联到9个级别,但在这里我们看到更深的调用堆栈.如果我们覆盖限制,-XX:MaxInlineLevel=20分数会变得更好一些:
Benchmark Mode Cnt Score Error Units
Contains.lambdaArrayStream avgt 5 33,294 ± 0,367 ns/op (was 53,242)
Contains.naive avgt 5 5,822 ± 0,207 ns/op
Run Code Online (Sandbox Code Playgroud)
for迭代数组是一个微不足道的计数循环.JVM可以在这里应用各种循环优化:循环剥离,循环展开等.这对于方法中的while-kind循环不起作用,该循环forEachWithCancel用于遍历IntStream.可以使用以下方法测量循环优化的效果-XX:LoopUnrollLimit=0 -XX:-UseLoopPredicate:
Benchmark Mode Cnt Score Error Units
Contains.lambdaArrayStream avgt 5 33,153 ± 0,559 ns/op
Contains.naive avgt 5 9,853 ± 0,150 ns/op (was 5,876)
Run Code Online (Sandbox Code Playgroud)
有是一些开销,构建和遍历流,但这是完全了解,不能认为是一个错误.我不会说开销很大(甚至50 ns/op也不是那么多); 但是,在这个特定的例子中,由于极小的工作量,开销占主导地位.
| 归档时间: |
|
| 查看次数: |
937 次 |
| 最近记录: |