【问题标题】:Unexpected parallelstream performance in Java 8Java 8 中意外的并行流性能
【发布时间】:2014-10-27 21:52:11
【问题描述】:

在 Iterable 上使用使用 spliterator() 创建的流时,我遇到了性能问题。即,像StreamSupport.stream(integerList.spliterator(), true)。想通过正常的收藏来证明这一点。请参阅下面的一些基准测试结果。

问题: 为什么从可迭代创建的并行流比从 ArrayList 或 IntStream 创建的流慢得多?

从一个范围

 public void testParallelFromIntRange() {
    long start = System.nanoTime();
    IntStream stream = IntStream.rangeClosed(1, Integer.MAX_VALUE).parallel();
    System.out.println("Is Parallel: "+stream.isParallel());
    stream.forEach(ParallelStreamSupportTest::calculate);
    long end = System.nanoTime();
    System.out.println("ParallelStream from range Takes : " + TimeUnit.MILLISECONDS.convert((end - start),
            TimeUnit.NANOSECONDS) + " milli seconds");
}

平行:真
ParallelStream 范围需要:490 毫秒

来自可迭代对象

 public void testParallelFromIterable() {
    Set<Integer> integerList = ContiguousSet.create(Range.closed(1, Integer.MAX_VALUE), DiscreteDomain.integers());
    long start = System.nanoTime();
    Stream<Integer> stream = StreamSupport.stream(integerList.spliterator(), true);
    System.out.println("Is Parallel: " + stream.isParallel());
    stream.forEach(ParallelStreamSupportTest::calculate);
    long end = System.nanoTime();
    System.out.println("ParallelStream from Iterable Takes : " + TimeUnit.MILLISECONDS.convert((end - start),
            TimeUnit.NANOSECONDS) + " milli seconds");
}

平行:真
来自 Iterable 的 ParallelStream 需要:12517 毫秒

还有这么简单的计算方法。

public static Integer calculate(Integer input) {
    return input + 2;
}

【问题讨论】:

  • ContinguousSet 实际上有正确的spliterator 吗? Guava 没有针对 Java 8 进行优化,而 JDK 显然是。
  • ContiguousSet.spliterator() 是否返回一个大小的拆分器,或者一个未知大小的拆分器?这可能会影响数据在线程之间的拆分方式。您可以使用Spliterators.spliterator(Iterator, long, int) 根据迭代器和集合的大小创建拆分器。
  • Iterable 在第二个示例中在哪里发挥作用?如果其中有一个,请注意 Iterable 本质上是顺序的,因为对元素的唯一访问是通过 hasNext/next

标签: java parallel-processing java-8 java-stream


【解决方案1】:

并非所有拆分器都是平等创建的。拆分器的任务之一是将源分解为可以并行处理的两部分。一个好的拆分器会将源大致分成两半(并且能够继续递归地这样做。)

现在,假设您正在为仅由迭代器描述的源编写拆分器。你能得到什么质量的分解?基本上,您所能做的就是将源分为“第一”和“休息”。这已经很糟糕了。结果是一个非常“右重”的计算树。

您从数据结构中获得的拆分器有更多用途;它知道数据的布局,并且可以使用它来提供更好的拆分,从而获得更好的并行性能。 ArrayList 的拆分器始终可以一分为二,并保留每一半中究竟有多少数据的知识。这非常好。平衡树的拆分器可以获得良好的分布(因为树的每一半都有大约一半的元素),但不如 ArrayList 拆分器好,因为它不知道确切的大小。 LinkedList 的拆分器和它一样糟糕。它所能做的就是(首先,休息)。从迭代器派生拆分器也是如此。

现在,一切都不一定会丢失;如果每个元素的工作量很高,则可以克服不良分裂。但是,如果您对每个元素进行少量工作,您将受到拆分器的拆分质量的限制。

【讨论】:

    【解决方案2】:

    您的基准测试存在几个问题。

    1. 由于装箱开销,Stream&lt;Integer&gt; 无法与 IntStream 进行比较。
    2. 你没有对计算结果做任何事情,这使得很难知道代码是否真的在运行
    3. 您正在使用System.nanoTime 进行基准测试,而不是使用适当的基准测试工具。

    这是一个基于 JMH 的基准测试:

    import com.google.common.collect.ContiguousSet;
    import com.google.common.collect.DiscreteDomain;
    import com.google.common.collect.Range;
    import java.util.stream.IntStream;
    import java.util.stream.Stream;
    import org.openjdk.jmh.annotations.Benchmark;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.OptionsBuilder;
    
    public class Ranges {
    
        final static int SIZE = 10_000_000;
    
        @Benchmark
        public long intStream() {
            Stream<Integer> st = IntStream.rangeClosed(1, SIZE).boxed();
    
            return st.parallel().mapToInt(x -> x).sum();
        }
    
        @Benchmark
        public long contiguousSet() {
            ContiguousSet<Integer> cs = ContiguousSet.create(Range.closed(1, SIZE), DiscreteDomain.integers());
            Stream<Integer> st = cs.stream();
    
            return st.parallel().mapToInt(x -> x).sum();
        }
    
        public static void main(String[] args) throws RunnerException {
            new Runner(
                    new OptionsBuilder()
                    .include(".*Ranges.*")
                    .forks(1)
                    .warmupIterations(5)
                    .measurementIterations(5)
                    .build()
            ).run();
        }
    }
    

    还有输出:

    Benchmark                  Mode   Samples        Score  Score error    Units
    b.Ranges.contiguousSet    thrpt         5       13.540        0.924    ops/s
    b.Ranges.intStream        thrpt         5       27.047        5.119    ops/s
    

    所以IntStream.range 的速度大约是ContiguousSet 的两倍,这是完全合理的,因为 ContiguousSet 没有实现自己的 Spliterator 并使用来自 Set 的默认值

    【讨论】:

    • 感谢您提供有关 JMH 的提示。我不知道它的存在。
    猜你喜欢
    • 2014-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-27
    • 2012-08-27
    相关资源
    最近更新 更多