关于效率: .filter(Optional::isPresent).map(Optional::get) 不是比 .flatmap(Optional::stream) 好吗?
Regarding efficiency: Isn’t .filter(Optional::isPresent).map(Optional::get) better than .flatmap(Optional::stream)?
Optional::stream
returns 包含值(如果存在)的 Stream,否则为空流。所以对于 Stream<Optional<T>> optionals
,
optionals.flatMap(Optional::stream)
returns a Stream<T>
包含所有可选值的现值。但是关于它背后的功能,我不确定为每个现值创建一个自己的流然后平面映射流的流的效率如何。
但即使在 documentation 中也将其作为预期用途提及。
与首先过滤所有当前值然后将可选值映射到它们的值相比,为什么流式传输可选值流不是非常低效?
我很好奇并使用 JMH.
编写了一个简单的微基准测试
事实证明,与 filter(Optional::isPresent).map(Optional::get)
相比,使用 flatMap(Optional::stream)
会导致相当严重的性能损失。
Java 16引入了mapMulti
,在用法上与flatMap
相似,性能特点与filter
/map
非常接近。
我的每个基准测试方法都采用 Optional<Integer>
的列表并计算所有现值的总和。
我实施了三种方法:
请注意,我 没有 使用 flatMapToInt
或 mapMultiToInt
方法,这可能更有效,因为我不想关注 streams-over-wrapper 对象方面,只比较流在 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 压力增加,即使是空结果也是如此。
免责声明:这显然只是一个正在测试的虚构示例,基准尚未经过同行评审(尚未),所以不要接受这些结果对于 g运行ted 没有进一步调查。
下面的完整基准代码(请注意,我拒绝了一些 iterations/runtimes 以在合理的时间内获得响应并硬编码为使用 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();
}
}
Optional::stream
returns 包含值(如果存在)的 Stream,否则为空流。所以对于 Stream<Optional<T>> optionals
,
optionals.flatMap(Optional::stream)
returns a Stream<T>
包含所有可选值的现值。但是关于它背后的功能,我不确定为每个现值创建一个自己的流然后平面映射流的流的效率如何。
但即使在 documentation 中也将其作为预期用途提及。
与首先过滤所有当前值然后将可选值映射到它们的值相比,为什么流式传输可选值流不是非常低效?
我很好奇并使用 JMH.
编写了一个简单的微基准测试事实证明,与 filter(Optional::isPresent).map(Optional::get)
相比,使用 flatMap(Optional::stream)
会导致相当严重的性能损失。
Java 16引入了mapMulti
,在用法上与flatMap
相似,性能特点与filter
/map
非常接近。
我的每个基准测试方法都采用 Optional<Integer>
的列表并计算所有现值的总和。
我实施了三种方法:
请注意,我 没有 使用 flatMapToInt
或 mapMultiToInt
方法,这可能更有效,因为我不想关注 streams-over-wrapper 对象方面,只比较流在 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 压力增加,即使是空结果也是如此。
免责声明:这显然只是一个正在测试的虚构示例,基准尚未经过同行评审(尚未),所以不要接受这些结果对于 g运行ted 没有进一步调查。
下面的完整基准代码(请注意,我拒绝了一些 iterations/runtimes 以在合理的时间内获得响应并硬编码为使用 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();
}
}