繁体   English   中英

在多线程的情况下,最好使用性能限制在流上使用限制

[英]Best way performance wise to use limit on stream in case of multithreading

我观看了JoséPaumard在InfoQ上的演讲: http : //www.infoq.com/fr/presentations/jdk8-lambdas-streams-collectors (法语)

事情是我坚持这一点。 要使用流多线程收集1M Long 我们可以这样做:

Stream<Long> stream = 
  Stream.generate(() -> ThreadLocalRandom.current().nextLong()) ;

List<Long> list1 = 
  stream.parallel().limit(10_000_000).collect(Collectors.toList()) ;

但是考虑到线程总是在检查上述限制会妨碍性能。

在那个谈话中,我们还看到了第二个解决方案:

Stream<Long> stream = 
  ThreadLocalRandom.current().longs(10_000_000).mapToObj(Long::new) ;

List<Long> list = 
  stream.parallel().collect(Collectors.toList()) ;

并且似乎是更好的性能明智的选择。

所以这是我的问题:为什么第二个代码更好,并且有更好或更便宜的方法呢?

这是依赖实现的限制。 对于并行性能,开发人员必须了解的一件事是,可预测的流大小通常有助于并行性能,因为它们可以平衡工作负载。

这里的问题是,通过Stream.generate()limit()创建的无限流的组合不会产生具有可预测大小的流,尽管它对我们而言似乎是完全可预测的。

我们可以使用以下辅助方法对其进行检查:

static void sizeOf(String op, IntStream stream) {
    final Spliterator.OfInt s = stream.spliterator();
    System.out.printf("%-18s%5d, %d%n", op, s.getExactSizeIfKnown(), s.estimateSize());
}

然后

sizeOf("randoms with size", ThreadLocalRandom.current().ints(1000));
sizeOf("randoms with limit", ThreadLocalRandom.current().ints().limit(1000));
sizeOf("range", IntStream.range(0, 100));
sizeOf("range map", IntStream.range(0, 100).map(i->i));
sizeOf("range filter", IntStream.range(0, 100).filter(i->true));
sizeOf("range limit", IntStream.range(0, 100).limit(10));
sizeOf("generate limit", IntStream.generate(()->42).limit(10));

将打印

randoms with size  1000, 1000
randoms with limit   -1, 9223372036854775807
range               100, 100
range map           100, 100
range filter         -1, 100
range limit          -1, 100
generate limit       -1, 9223372036854775807

因此,我们看到,某些源(例如Random.ints(size)IntStream.range(…)产生的流具有可预测的大小,并且某些中间操作(例如map能够携带信息,因为他们知道大小不受影响。 其他(例如filterlimit不会传播大小(作为已知的确切大小)。

显然, filter无法预测实际的元素数量,但是它提供了源大小作为估计,这是合理的,因为这是可以通过过滤器的最大元素数量。

相反,即使源具有确切的大小,并且当前我们知道可预测的大小与min(source size, limit)一样简单,当前的limit实现也不提供大小。 取而代之的是,它甚至报告了一个毫无意义的估计大小(源的大小),尽管已知结果大小永远不会超过限制。 在无限流的情况下,我们还有一个障碍,即基于流的Spliterator接口无法报告它是无限的。 在这些情况下,无限流+极限将返回Long.MAX_VALUE作为估计值,这意味着“我什至无法猜测”。

因此,根据经验,在当前实现中,程序员应避免在可以预先在流源处指定所需大小的情况下使用limit 但是,由于在有序并行流(不适用于随机变量或generate )的情况下, limit也具有明显的(已记录的)缺点,因此大多数开发人员无论如何都会避免使用limit

为什么第二个代码更好?

在第一种情况下,您将创建无限源,将其拆分以并行执行一堆任务,每个任务提供无限数量的元素,然后限制结果的整体大小 即使源是无序的,这也意味着一些开销。 在这种情况下,各个任务应相互交谈以检查何时达到整体大小。 如果他们经常交谈,这会增加争论。 如果他们少说话,他们实际上会产生超出必要数量的数字,然后丢弃其中一些数字。 我相信,实际的流API实现是在任务之间进行较少的交谈,但这实际上导致产生不必要的数量。 这也会增加内存消耗并激活垃圾收集器。

相反,在第二种情况下,您将创建一个已知大小的有限来源。 将任务拆分为子任务时,它们的大小也很明确,并且总的来说,它们会精确地生成所需数量的随机数,而无需彼此交谈。 这就是为什么它更快。

有没有更好,或更便宜的方法呢?

您的代码示例中最大的问题是装箱。 如果您需要10_000_000个随机数,则将它们每个都装箱并存储在List<Long>中是一个非常糟糕的主意:创建大量不必要的对象,执行许多堆分配等等。 用原始流替换它:

long[] randomNumbers = ThreadLocalRandom.current().longs(10_000_000).parallel().toArray();

这会快得多(可能是一个数量级)。

您也可以考虑使用新的Java-8 SplittableRandom类。 它提供大致相同的性能,但生成的随机数具有更高的质量(包括通过DieHarder 3.31.1 ):

long[] randomNumbers = new SplittableRandom().longs(10_000_000).parallel().toArray();

JDK文档对此行为有很好的解释,因为排序约束会降低并行处理的性能

来自文档的限制功能文档文本-https: //docs.oracle.com/javase/8/docs/api/java/util/stream/LongStream.html

虽然limit()在顺序流管道上通常是便宜的操作,但在有序并行管道上可能会非常昂贵,尤其是对于maxSize较大的情况,因为limit(n)不仅要返回任何n个元素,而且还要返回前n个元素遇到顺序中的元素。 如果情况的语义允许,则使用无序流源(例如generate(LongSupplier))或通过BaseStream.unordered()删除排序约束可能会导致并行管道中limit()的显着加速。 如果需要与遇到顺序保持一致,并且在并行管道中使用limit()遇到性能低下或内存使用率不足的情况,则切换为使用sequence()顺序执行可能会提高性能。 块引用

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM