这是我最近发现的另一种技术:
Collections.nCopies(8, 1)
.stream()
.forEach(i -> System.out.println(i))
Collections.nCopies
调用会创建一个包含
n
个给定值的
List
,在这里是封装了整数值 1 的
Integer
。它实际上并没有创建一个具有
n
个元素的列表,而是创建了一个"虚拟化"列表,只包含该值和长度,对范围内任何
get
调用都返回该值。自 JDK 1.2 引入 Collections Framework 以来,
nCopies
方法一直存在。当然,从其结果创建流的能力是在 Java SE 8 中添加的。
这种技术比
IntStream.generate
和
IntStream.iterate
方法更快,令人惊讶的是,它也比
IntStream.range
方法更快。
对于
iterate
和
generate
的结果可能并不令人感到意外。流框架(实际上是这些流的 Spliterators)是建立在 lambda 可能每次生成不同值的假设上,并且它们将生成数量不确定的结果。这使得并行拆分特别困难。对于这种情况,
iterate
方法也存在问题,因为每个调用都需要前一个的结果。因此使用
generate
和
iterate
的流不适合生成重复的常量。
range
的相对性能较差是令人惊讶的。它也是虚拟化的,因此实际上并没有所有元素都存在于内存中,并且大小是预先知道的。这应该使得它成为一个快速且容易并行的 Spliterator。但令人惊讶的是它表现并不好。也许原因在于
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
来实现的!我原以为需要创建一个专门的spliterator才能使它跑得更快,但它已经表现得相当优秀了。