Java8: Streams vs Collection 的性能

我是 Java 8的新手。我仍然不太了解这个 API 的深度,但是我已经做了一个非正式的基准测试来比较新的 Streams API 和旧的 CollectionsAPI 的性能。

该测试包括过滤一个 Integer列表,对于每个偶数,计算平方根并将其存储在 Double的结果 List中。

密码如下:

    public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 1000000;


List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}


List<Double> result = new LinkedList<>();




//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add(Math.sqrt(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));




//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));




//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}.

以下是双核机器的结果:

    Collections: Elapsed time:        94338247 ns   (0,094338 seconds)
Streams: Elapsed time:           201112924 ns   (0,201113 seconds)
Parallel streams: Elapsed time:  357243629 ns   (0,357244 seconds)

对于这个特定的测试,流的速度是集合的两倍,并且并行性没有帮助(或者我用错了方法?).

问题:

  • 这个测试公平吗? 我做错了什么吗?
  • 流是否比集合慢? 有人在这方面做了一个很好的正式基准测试吗?
  • 我应该努力采用哪种方法?

最新结果。

按照@pveentjer 的建议,在 JVM 预热(1k 次迭代)之后,我运行了1k 次测试:

    Collections: Average time:      206884437,000000 ns     (0,206884 seconds)
Streams: Average time:           98366725,000000 ns     (0,098367 seconds)
Parallel streams: Average time: 167703705,000000 ns     (0,167704 seconds)

在这种情况下,流的性能更好。我想知道在一个过滤函数只在运行时调用一次或两次的应用程序中会观察到什么。

117848 次浏览

对于你正在尝试做的事情,我不会使用普通的 java api 的无论如何。有一吨的装箱/拆箱正在进行,所以有一个巨大的性能开销。

我个人认为很多 API 设计都是垃圾,因为他们创建了很多对象垃圾。

尝试使用 double/int 的基本数组,并尝试使用单线程,看看性能如何。

您可能需要查看 JMH 来处理基准测试。它解决了一些典型的陷阱,比如对 JVM 进行预热。

1)使用你的基准测试,你可以看到时间少于1秒。这意味着副作用会对你的结果产生很大的影响。所以,我把你的任务增加了10倍

    int max = 10_000_000;

然后运行你的基准测试,我的结果是:

Collections: Elapsed time:   8592999350 ns  (8.592999 seconds)
Streams: Elapsed time:       2068208058 ns  (2.068208 seconds)
Parallel streams: Elapsed time:  7186967071 ns  (7.186967 seconds)

未经编辑(int max = 1_000_000)的结果是

Collections: Elapsed time:   113373057 ns   (0.113373 seconds)
Streams: Elapsed time:       135570440 ns   (0.135570 seconds)
Parallel streams: Elapsed time:  104091980 ns   (0.104092 seconds)

这就像你的结果: 流比收集慢。 结论:花了很多时间用于流初始化/值传输。

2)增加任务流后,任务流变得更快(没关系) ,但并行流仍然太慢。怎么了?注意: 命令中有 collect(Collectors.toList())。从集合到单个集合实质上会引入性能瓶颈和并发执行情况下的开销。通过替换可以估计间接费用的相对成本

collecting to collection -> counting the element count

对于流,它可以通过 collect(Collectors.counting())来完成。我得到了结果:

Collections: Elapsed time:   41856183 ns    (0.041856 seconds)
Streams: Elapsed time:       546590322 ns   (0.546590 seconds)
Parallel streams: Elapsed time:  1540051478 ns  (1.540051 seconds)

这可是个大任务!(int max = 10000000) 结论:收集项目收集花费了大部分时间。最慢的部分是添加到列表中。顺便说一下,简单的 ArrayList用于 Collectors.toList()

  1. 除了使用迭代器从列表中间大量删除之外,不要使用 LinkedList

  2. 停止手工编写基准测试代码,使用 JMH

适当基准:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
public static final int N = 10000;


static List<Integer> sourceList = new ArrayList<>();
static {
for (int i = 0; i < N; i++) {
sourceList.add(i);
}
}


@Benchmark
public List<Double> vanilla() {
List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
for (Integer i : sourceList) {
if (i % 2 == 0){
result.add(Math.sqrt(i));
}
}
return result;
}


@Benchmark
public List<Double> stream() {
return sourceList.stream()
.filter(i -> i % 2 == 0)
.map(Math::sqrt)
.collect(Collectors.toCollection(
() -> new ArrayList<>(sourceList.size() / 2 + 1)));
}
}

结果:

Benchmark                   Mode   Samples         Mean   Mean error    Units
StreamVsVanilla.stream      avgt        10       17.588        0.230    ns/op
StreamVsVanilla.vanilla     avgt        10       10.796        0.063    ns/op

正如我预期的那样,流实现相当慢。JIT 能够内联所有的 lambda 内容,但是不能产生像香草版本那样完美简洁的代码。

一般来说,Java8流并不神奇。它们不能加速已经实现良好的事情(可能使用纯迭代或 Java5的 for-each 语句替换为 Iterable.forEach()Collection.removeIf()调用)。流更多的是关于编码的方便性和安全性。方便,速度权衡在这里很管用。

    public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 10000000;


List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}


List<Double> result = new LinkedList<>();




//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add( doSomeCalculate(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));




//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));




//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i ->  doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}


static double doSomeCalculate(int input) {
for(int i=0; i<100000; i++){
Math.sqrt(i+input);
}
return Math.sqrt(input);
}

我稍微修改了一下代码,运行在我的有8个内核的 Mac Book pro 上,我得到了一个合理的结果:

Collections: Elapsed time:      1522036826 ns   (1.522037 seconds)
Streams: Elapsed time:          4315833719 ns   (4.315834 seconds)
Parallel streams: Elapsed time:  261152901 ns   (0.261153 seconds)

对 Java8和 Java11的有趣结果。我使用的代码,由 leventov 提供,几乎没有修改:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(BenchmarkMain.N)
public class BenchmarkMain {


public static final int N = 10000;


static List<Integer> sourceList = new ArrayList<>();
static {
for (int i = 0; i < N; i++) {
sourceList.add(i);
}
}


@Benchmark
public List<Double> vanilla() {
List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
for (Integer i : sourceList) {
if (i % 2 == 0){
result.add(Math.sqrt(i));
}
}
return result;
}


@Benchmark
public List<Double> stream() {
return sourceList.stream()
.filter(i -> i % 2 == 0)
.map(Math::sqrt)
.collect(Collectors.toCollection(
() -> new ArrayList<>(sourceList.size() / 2 + 1)));
}


/**
* @param args the command line arguments
*/
public static void main(String[] args) throws IOException {
org.openjdk.jmh.Main.main(args);


}


}

爪哇8:

# JMH version: 1.31
# VM version: JDK 1.8.0_262, OpenJDK 64-Bit Server VM, 25.262-b19
# VM invoker: /opt/jdk1.8.0_262/jre/bin/java
# VM options: <none>
# Blackhole mode: full + dont-inline hint
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
...
Benchmark              Mode  Cnt   Score   Error  Units
BenchmarkMain.stream   avgt   25  10.680 ± 0.744  ns/op
BenchmarkMain.vanilla  avgt   25   6.490 ± 0.159  ns/op

爪哇11:

# JMH version: 1.31
# VM version: JDK 11.0.2, OpenJDK 64-Bit Server VM, 11.0.2+9
# VM invoker: /opt/jdk-11.0.2/bin/java
# VM options: <none>
# Blackhole mode: full + dont-inline hint
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
...
Benchmark              Mode  Cnt  Score   Error  Units
BenchmarkMain.stream   avgt   25  5.521 ± 0.057  ns/op
BenchmarkMain.vanilla  avgt   25  7.359 ± 0.118  ns/op

使用 Java17我的结果

Collections: Elapsed time:109585000 ns  (0.109585 seconds)
Streams: Elapsed time:42179700 ns   (0.042180 seconds)
Parallel streams: Elapsed time:76177100 ns  (0.076177 seconds)

代替 LinkedList 使用 List.of结果更改

Collections: Elapsed time:49681300 ns   (0.049681 seconds)
Streams: Elapsed time:38930300 ns   (0.038930 seconds)
Parallel streams: Elapsed time:49190500 ns  (0.049191 seconds)