这是我前几天遇到的另一种技术:
Collections.nCopies(8, 1)
.stream()
.forEach(i -> System.out.println(i));
该Collections.nCopies
调用会创建一个List
包含n
您提供的任何值的副本。在这种情况下,它是装箱Integer
值 1。当然,它实际上并没有创建包含n
元素的列表;它创建一个仅包含值和长度的“虚拟化”列表,并且对get
范围内的任何调用都只返回该值。自 JDK 1.2 中引入 Collections Framework 以来,该nCopies
方法就一直存在。当然,在 Java SE 8 中添加了从结果创建流的功能。
重要的是,另一种方法可以在大约相同的行数中做同样的事情。
但是,这种技术比IntStream.generate
andIntStream.iterate
方法更快,而且令人惊讶的是,它也比IntStream.range
方法更快。
因为iterate
和generate
结果也许并不太令人惊讶。流框架(实际上,这些流的拆分器)是建立在 lambda 每次可能生成不同值的假设之上,并且它们将生成无限数量的结果。这使得并行拆分特别困难。该iterate
方法在这种情况下也存在问题,因为每次调用都需要前一次调用的结果。因此,使用generate
and的流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
! 我曾预计有必要创建一个专门的拆分器以使其快速运行,但它似乎已经相当不错了。