[英]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
能够携带信息,因为他们知道大小不受影响。 其他(例如filter
和limit
不会传播大小(作为已知的确切大小)。
显然, 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.