Java Stream.concat VS Collection.addAll的性能

ala*_*678 6 java java-8 java-stream

用于在流中组合两组数据.

Stream.concat(stream1, stream2).collect(Collectors.toSet());
Run Code Online (Sandbox Code Playgroud)

要么

stream1.collect(Collectors.toSet())
       .addAll(stream2.collect(Collectors.toSet()));
Run Code Online (Sandbox Code Playgroud)

哪个更有效,为什么?

NoD*_*und 9

出于可读性和意图的考虑,Stream.concat(a, b).collect(toSet())比第二种方案更清晰.

为了问题," 什么是最有效的 ",这里是一个JMH测试(我想说我没有那么多使用JMH,可能有一些空间来改进我的基准测试):

使用JMH,使用以下代码:

package stackoverflow;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Fork(1)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode({ Mode.AverageTime})
public class StreamBenchmark {
  private Set<String> s1;
  private Set<String> s2;

  @Setup
  public void setUp() {
    final Set<String> valuesForA = new HashSet<>();
    final Set<String> valuesForB = new HashSet<>();
    for (int i = 0; i < 1000; ++i) {
      valuesForA.add(Integer.toString(i));
      valuesForB.add(Integer.toString(1000 + i));
    }
    s1 = valuesForA;
    s2 = valuesForB;
  }

  @Benchmark
  public void stream_concat_then_collect_using_toSet(final Blackhole blackhole) {
    final Set<String> set = Stream.concat(s1.stream(), s2.stream()).collect(Collectors.toSet());
    blackhole.consume(set);
  }

  @Benchmark
  public void s1_collect_using_toSet_then_addAll_using_toSet(final Blackhole blackhole) {
    final Set<String> set = s1.stream().collect(Collectors.toSet());
    set.addAll(s2.stream().collect(Collectors.toSet()));
    blackhole.consume(set);
  }
}
Run Code Online (Sandbox Code Playgroud)

你得到了这些结果(为了便于阅读,我省略了一些部分).

Result "s1_collect_using_toSet_then_addAll_using_toSet":
  156969,172 ±(99.9%) 4463,129 ns/op [Average]
  (min, avg, max) = (152842,561, 156969,172, 161444,532), stdev = 2952,084
  CI (99.9%): [152506,043, 161432,301] (assumes normal distribution)

Result "stream_concat_then_collect_using_toSet":
  104254,566 ±(99.9%) 4318,123 ns/op [Average]
  (min, avg, max) = (102086,234, 104254,566, 111731,085), stdev = 2856,171
  CI (99.9%): [99936,443, 108572,689] (assumes normal distribution)
# Run complete. Total time: 00:00:25

Benchmark                                                       Mode  Cnt       Score      Error  Units
StreamBenchmark.s1_collect_using_toSet_then_addAll_using_toSet  avgt   10  156969,172 ± 4463,129  ns/op
StreamBenchmark.stream_concat_then_collect_using_toSet          avgt   10  104254,566 ± 4318,123  ns/op
Run Code Online (Sandbox Code Playgroud)

使用的版本Stream.concat(a, b).collect(toSet())应该更快(如果我读好JMH数字).

另一方面,我认为这个结果是正常的,因为你没有创建一个中间集(这有一些成本,即使有HashSet),并且正如在第一个答案的评论中所述,它Stream懒惰的连接.

使用分析器,您可能会看到哪个部分较慢.您可能还想使用toCollection(() -> new HashSet(1000))而不是toSet()查看问题在于增长HashSet内部哈希数组.


Tim*_*kle 6

您的问题称为过早优化。永远不要仅仅因为您认为它更快而选择一种语法而不是另一种。始终使用最能表达您的意图并支持理解您的逻辑的语法。


你对我正在做的任务一无所知 – alan7678

确实如此。

但我不需要。

一般有两种情况:

  1. 您开发一个OLTP应用程序。在这种情况下,应用程序应该在一秒或更短时间内响应。用户不会体验到您呈现的变体之间的性能差异。

  2. 您开发了某种批处理,它将在无人看管的情况下运行一段时间。在这种情况下,性能差异“可能”很重要,但前提是您需要为批处理运行的时间付费。

无论哪种方式:真正的性能问题(通过倍数而不是分数来加速应用程序)通常是由您实现的逻辑引起的(例如:过度通信、“隐藏循环”或过多的对象创建)。
这些问题通常无法通过选择某种语法来解决或避免。

如果为了性能提升而忽略可读性,则会使应用程序更难维护。
更改难以维护的代码库很容易烧掉大量本可以节省的资金,因为程序在应用程序的生命周期内通过使用可读性较差但速度稍快的语法提高了速度。

毫无疑问,这个问题在某些情况下对其他人也很重要。– alan7678

毫无疑问,人们很好奇。

幸运的是,我更喜欢的语法似乎也表现得更好。– alan7678

如果你知道,你为什么要问?

能否与您分享您的测量结果以及您的测量设置?

更重要的是:这对 Java9 或 Java10 有效吗?

Java 的性能主要来自 JVM 实现,这可能会发生变化。当然,较新的语法结构(如 java 流)更有可能带来新的 java 版本带来的性能提升。但是不能保证...

在我的情况下,对性能的需求大于可读性的差异。– alan7678

5 年后您仍会负责此申请吗?或者你是一名顾问,在开始一个项目然后切换到下一个项目时获得报酬?

我从来没有一个项目可以在语法级别解决我的性能问题。
但我一直在使用存在 10 多年的遗留代码,这很难维护,因为有人不尊重可读性。

所以你的不回答不适用于我。– alan7678

这是一个自由的世界,任你挑选。

  • 您对我正在从事的任务一无所知,毫无疑问,这个问题在某些情况下对其他人也很重要。幸运的是,我更喜欢的语法似乎也表现得更好。就我而言,对性能的需求大于可读性的差异。所以你的不回答不适用于我。 (3认同)
  • 我正在优化一个 flink 算法并偶然发现了这个问题,这非常相关。 (2认同)

Hol*_*ger 5

首先必须强调,第二种变体是不正确的。收集toSet()器返回\xe2\x80\x9cnoSet保证类型、可变性、可序列化性或线程安全性\xe2\x80\x9d。如果不能保证可变性,则调用是不正确的addAll如果不能保证可变性,则在结果上Set

\n\n

它恰好适用于当前版本的参考实现,其中HashSet将创建 a,但可能会在未来版本或替代实现中停止工作。为了解决这个问题,您必须替换toSet()toCollection(HashSet::new)一个 Stream\xe2\x80\x99scollect操作。

\n\n

这导致第二个变体不仅在当前实现中效率较低,如本答案toSet()所示,它还可能通过坚持结果是精确类型来阻止未来对收集器进行的优化HashSet。此外,与toSet()收集器不同,toCollection(\xe2\x80\xa6)收集器无法检测目标集合是否无序,这可能在未来的实现中具有性能相关性。

\n