这是我前几天遇到的另一种技术:
Collections.nCopies(8, 1)
.stream()
.forEach(i -> System.out.println(i));
该Collections.nCopies
调用将创建一个List
包含n
您提供的任何值的副本。在这种情况下,它是装箱的Integer
值1 n
。它创建一个“虚拟化”列表,其中仅包含值和长度,并且对get
范围内的任何调用都将返回该值。nCopies
自从JDK 1.2引入Collections Framework以来,这种方法就一直存在。当然,Java SE 8中增加了根据结果创建流的功能。
没什么大不了的,另一种方法可以在大约相同的行数中完成相同的操作。
但是,此技术比IntStream.generate
和IntStream.iterate
方法快,而且令人惊讶的是,它也比IntStream.range
方法快。
对于iterate
,generate
结果也许并不奇怪。流框架(实际上是这些流的拆分器)是基于以下假设而建立的:lambda每次可能会生成不同的值,并且它们将生成无数的结果。这使得并行分裂特别困难。iterate
对于这种情况,该方法也有问题,因为每个调用都需要前一个的结果。因此,使用generate
和的流iterate
在生成重复常数方面效果不佳。
相对较差的性能range
令人惊讶。这也是虚拟化的,因此元素实际上并不全部存在于内存中,并且大小是预先知道的。这应该构成一个快速且易于并行化的分离器。但是令人惊讶的是,它做得不好。可能的原因是range
必须为范围的每个元素计算一个值,然后在其上调用一个函数。但是这个函数只是忽略它的输入并返回一个常量,所以令我惊讶的是它没有被内联和杀死。
该Collections.nCopies
技术必须进行装箱/拆箱操作才能处理这些值,因为没有的原始专长List
。由于每次的值都相同,因此基本上将其装箱一次,并且所有n
副本共享该箱。我怀疑装箱/拆箱是高度优化的,甚至是内在的,并且可以很好地内联。
这是代码:
public static final int LIMIT = 500_000_000;
public static final long VALUE = 3L;
public long range() {
return
LongStream.range(0, LIMIT)
.parallel()
.map(i -> VALUE)
.map(i -> i % 73 % 13)
.sum();
}
public long ncopies() {
return
Collections.nCopies(LIMIT, VALUE)
.parallelStream()
.mapToLong(i -> i)
.map(i -> i % 73 % 13)
.sum();
}
这是JMH结果:(2.8GHz Core2Duo)
Benchmark Mode Samples Mean Mean error Units
c.s.q.SO18532488.ncopies thrpt 5 7.547 2.904 ops/s
c.s.q.SO18532488.range thrpt 5 0.317 0.064 ops/s
ncopies版本中存在相当大的差异,但总体而言,它似乎比range版本快20倍。(不过,我很愿意相信自己做错了。)
我对该nCopies
技术的效果感到惊讶。在内部,它并没有什么特别之处,只是使用IntStream.range
!来实现虚拟列表流。我曾期望有必要创建一个专门的分离器,以使其快速运行,但是看起来已经相当不错了。