当前位置: 首页 > 面试题库 >

为什么lambda IntStream.anyMatch()比朴素的实现慢10个?

裴俊智
2023-03-14
问题内容

我最近在分析代码,发现其中一个有趣的瓶颈。这是基准:

@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进行的操作比使用简单循环的朴素实现慢10倍。我知道lambda应该会慢一些。但是十倍?我做错了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

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

对象分配

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

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,每个对象都需要几纳秒的时间:

  • 拉姆达 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可以在此处应用广泛的循环优化:循环剥离,循环展开等。这不适用于用于遍历IntStream
while-kind循环forEachWithCancel方法。循环优化的效果可以用以下方法测量-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)

结论

一些开销,构建和遍历流,但这是完全了解,不能认为是一个错误。我不会说开销很大(即使50 ns /
op也不算多);但是,在此特定示例中,由于工作量极小,因此开销占主导。



 类似资料:
  • 本文向大家介绍为什么朴素贝叶斯如此朴素?相关面试题,主要包含被问及为什么朴素贝叶斯如此朴素?时的应答技巧和注意事项,需要的朋友参考一下 因为朴素贝叶斯有个重要的假设前提,也就是假设样本的所有特征之间是相互独立的,而这个在现实世界中是不真实的,因此说其很朴素

  • 为了好玩,我决定用红宝石编码伊拉托西筛子。只是为了好玩,因为我知道有一个库函数。而且,我认为它会很快。但我发现它并不是,至少在我的ruby 1.9.3中,我的上网本速度快了好几倍,甚至在c中也没有。为什么会这样呢。 库实现: 我在红宝石: 图书馆非常慢。

  • 我昨天对一个答案发表了评论,其中有人在正则表达式中使用了,而不是或。我说使用范围或数字说明符可能比使用字符集更快。 我决定今天测试一下,并惊讶地发现(至少在C#regex引擎中)似乎比其他两个似乎没有太大区别的任何一个都慢。这是我的测试输出超过10000个随机字符串,其中包含1000个随机字符,其中5077个实际上包含一个数字: 这对我来说是一个惊喜,有两个原因,如果有人能解释一下,我会很感兴趣:

  • 我发现 比 Python 2 和 3 中的函数慢。 Python 2 蟒蛇 3 为什么<code>max</code>(<code>O(n)</code>)比<code>sort</code>函数(<code<O(nlogn)</code>)慢?

  • 问题内容: 并且部分地收到了这个答案,这似乎表明bind应该比闭包更快: 范围遍历意味着,当您要获取存在于另一个范围中的值(变量,对象)时,因此会增加额外的开销(代码执行起来会变慢)。 使用bind,您正在使用现有范围调用函数,这样就不会发生范围遍历。 两个jsperfs表示bind实际上比闭包慢得多。 这是对以上内容的评论 而且,我决定编写自己的jsperf 那么,为什么结合速度这么慢(铬含量超