为什么lambda IntStream.anyMatch()比天真的实现慢10?

时间:2017-02-21 18:19:34

标签: 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;
    }

}

结果:

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

如果显示Array包含通过lambda的操作比使用简单循环的naive实现慢10倍。我知道lambdas应该慢一点。但是10次?我做错了lambda还是这是java的一些问题?

1 个答案:

答案 0 :(得分:6)

您的基准测试实际上并不衡量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

5.8 ns大约是2.4 GHz CPU的14个周期。工作量非常小,任何额外的周期都会很明显。那么流操作的开销是多少?

对象分配

现在用-prof gc profiler重新运行基准测试。它将显示堆分配的数量:

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

lambdaArrayStream每次迭代分配152个字节,而naive基准则不分配任何内容。当然,分配不是免费的:至少有5个对象被构造来支持anyMatch,每个对象需要几纳秒:

  • Lambda i -> i == val
  • IntPipeline.Head
  • Spliterators.IntArraySpliterator
  • MatchOps.MatchOp
  • MatchOps.MatchSink

调用堆栈

java.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)

并非所有这些方法调用都可以内联。此外,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

循环优化

对数组的

for迭代是一个微不足道的计数循环。 JVM可以在这里应用各种循环优化:循环剥离,循环展开等。这不适用于while - forEachWithCancel方法中的-XX:LoopUnrollLimit=0 -XX:-UseLoopPredicate类循环,用于遍历IntStream。可以使用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)

来衡量循环优化的效果
{{1}}

结论

构造和遍历流的一些开销,但这是完全理解的,不能被视为错误。我不会说开销很大(即使50 ns / op也不是那么多);但是,在这个特定的例子中,由于工作量非常小,开销占主导地位。