【问题标题】:Regarding efficiency: Isn’t .filter(Optional::isPresent).map(Optional::get) better than .flatmap(Optional::stream)?关于效率: .filter(Optional::isPresent).map(Optional::get) 不是比 .flatmap(Optional::stream) 好吗?
【发布时间】:2021-06-16 07:36:42
【问题描述】:

Optional::stream 返回一个包含该值的 Stream(如果存在),否则返回一个空流。所以对于Stream<Optional<T>> optionals

optionals.flatMap(Optional::stream)

返回一个 Stream<T> 包含所有选项的当前值。但是关于它背后的功能,我不确定为每个现值创建一个自己的流然后对流的流进行平面映射是多么有效。

但即使在documentation 中,这也被提及为预期用途。

与首先过滤所有当前值然后将选项映射到它们的值相比,为什么流式传输选项流效率不高?

【问题讨论】:

  • 目前尚不清楚最多包含一个元素的流的创建或访问成本是否应该很高。此类流的流可能不会比原始 Optional 实例的流更昂贵。
  • 照原样,我想说这是个人选择的问题,只要我们没有实际的性能问题(...stream().filter(Optional::isPresent).map(Optional::get)...stream().flatMap(Optional::stream))。请记住"premature optimization is the root of all evil" -- Donald Ervin Knuth: Computer Programming as an Art (1974), p. 671
  • 您(或某人)需要提出一个基准。我没有数字来支持我的主张,但我记得在使用 flatMap 选项时看到了严重的性能下降。我想主要因素是创建了许多新的Stream 实例,然后进行了垃圾收集。然后需要逐个迭代这些单项流中的每一个(通过获取其迭代器实例)

标签: java java-stream optional


【解决方案1】:

我很好奇并使用JMH 编写了一个简单的微基准测试。

事实证明,使用flatMap(Optional::stream) 比使用filter(Optional::isPresent).map(Optional::get) 有相当严重的性能损失。

Java 16 引入了mapMulti,在用法上与flatMap 相似,性能特征与filter/map 非常接近。

我的每个基准测试方法都采用Optional<Integer> 的列表并计算所有当前值的总和。

我实现了三种方法:

  1. flatMap 提出的问题
  2. filtermap 如问题中所述
  3. mapMulti 在 JDK 16 中引入。

请注意,我确实没有使用flatMapToIntmapMultiToInt 方法,这可能更有效,因为我不想专注于包装流对象方面并仅比较 Optional 对象上的流的使用情况。

对于所有方法,我使用一个完整列表(所有值都存在)、一个半空列表(每隔一个值存在)和一个完全空列表(每个可选值都是空的)运行基准测试。这些列表的长度都相同(每个列表任意选取 10 000 个元素)。

值的单位是 us/op(每个操作的微秒,意味着一个完整的流评估)。

Approach Full List Half Empty List Empty List
flatMap 207.219 ± 1.176 175.355 ± 4.955 142.986 ± 2.821
filter/map 12.856 ± 0.375 12.086 ± 0.451 6.856 ± 0.143
mapMulti 13.990 ± 0.353 11.685 ± 0.276 7.034 ± 0.199

请注意,这里的绝对数字是特定于我的运行 JDK 16 的机器,而且大多数情况下都无关紧要。相对差异在这里很重要。

似乎flatMap 方法既慢得多,而且变化多端。如果我不得不猜测可变性来自于创建的所有 Stream 对象导致的 GC 压力增加,即使是空结果也是如此。

免责声明:这显然只是一个正在测试的虚构示例,并且基准尚未经过同行评审(尚未),因此在没有进一步调查的情况下不要认为这些结果是理所当然的.

下面的完整基准代码(请注意,我拒绝了一些迭代/运行时以在合理的时间内获得响应,并硬编码为使用 4 个线程。根据需要进行调整。)

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Fork(value = 1, warmups = 0)
@Warmup(iterations = 5, time = 5)
@Measurement(iterations = 5, time = 5)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Threads(4)
public class MyBenchmark {

    @State(Scope.Benchmark)
    public static class MyLists {
        private static final int LIST_SIZE = 10_000;

        public final List<Optional<Integer>> allValues;
        public final List<Optional<Integer>> halfEmpty;
        public final List<Optional<Integer>> allEmpty;

        public MyLists() {
            List<Optional<Integer>> allValues = new ArrayList<>(LIST_SIZE);
            List<Optional<Integer>> halfEmpty = new ArrayList<>(LIST_SIZE);
            List<Optional<Integer>> allEmpty = new ArrayList<>(LIST_SIZE);
            for (int i = 0; i < LIST_SIZE; i++) {
                Optional<Integer> o = Optional.of(i);
                allValues.add(o);
                halfEmpty.add(i % 2 == 0 ? o : Optional.empty());
                allEmpty.add(Optional.empty());
            }
            this.allValues = Collections.unmodifiableList(allValues);
            this.halfEmpty = Collections.unmodifiableList(halfEmpty);
            this.allEmpty = Collections.unmodifiableList(allEmpty);
        }
    }

    @Benchmark
    public long filter_and_map_allValues(MyLists lists) {
        return filterAndMap(lists.allValues);
    }

    @Benchmark
    public long filter_and_map_halfEmpty(MyLists lists) {
        return filterAndMap(lists.halfEmpty);
    }

    @Benchmark
    public long filter_and_map_allEmpty(MyLists lists) {
        return filterAndMap(lists.allEmpty);
    }

    @Benchmark
    public long flatMap_allValues(MyLists lists) {
        return flatMap(lists.allValues);
    }

    @Benchmark
    public long flatMap_halfEmpty(MyLists lists) {
        return flatMap(lists.halfEmpty);
    }

    @Benchmark
    public long flatMap_allEmpty(MyLists lists) {
        return flatMap(lists.allEmpty);
    }


    @Benchmark
    public long mapMulti_allValues(MyLists lists) {
        return mapMulti(lists.allValues);
    }

    @Benchmark
    public long mapMulti_halfEmpty(MyLists lists) {
        return mapMulti(lists.halfEmpty);
    }

    @Benchmark
    public long mapMulti_allEmpty(MyLists lists) {
        return mapMulti(lists.allEmpty);
    }

    private long filterAndMap(List<Optional<Integer>> input) {
        return input.stream().filter(Optional::isPresent).map(Optional::get).mapToInt(Integer::intValue).sum();
    }

    private long flatMap(List<Optional<Integer>> input) {
        return input.stream().flatMap(Optional::stream).mapToInt(Integer::intValue).sum();
    }

    private long mapMulti(List<Optional<Integer>> input) {
        // Unfortunately the type witness <Integer> is necessary here, as type inference would otherwise make mapMulti produce a Stream<Object>.
        return input.stream().<Integer>mapMulti(Optional::ifPresent).mapToInt(Integer::intValue).sum();
    }
}

【讨论】:

  • 使用 JDK 16,一个新的变种是可能的,mapMulti(Optional::ifPresent)
  • @Holger:谢谢,我还不知道那个,我已经将它添加到基准测试中。它似乎表现得几乎和filter/map一样好。
  • 我们也可以使用 input.stream().mapMultiToInt((o,c) -&gt; o.ifPresent(c::accept)) .sum() 就地拆箱,但由于捕获 lambda 表达式,恐怕它不会提高性能(尽管当 Escape Analysis 工作得足够好时可能会这样)。
  • @Holger:我看到这是一个选项,但 1.) 不太知道如何使用它,并且 2.) 不想过多关注拆箱方面(因为'会改变问题/答案的重点)。当所有三种方法都做同样的事情时,比较结果会更容易。
猜你喜欢
  • 2021-01-12
  • 1970-01-01
  • 1970-01-01
  • 2014-05-08
  • 1970-01-01
  • 2022-01-17
  • 1970-01-01
  • 1970-01-01
  • 2019-07-29
相关资源
最近更新 更多