Iterator与Java的流8

Mig*_*boa 45 java domain-driven-design domain-model java-8 java-stream

为了利用java.util.streamJdk 8中包含的各种查询方法,我尝试设计域模型,其中具有*多重性(具有零个或多个实例)的关系的getter 返回a Stream<T>而不是Iterable<T>or Iterator<T>.

我怀疑的是,与此Stream<T>相比,Iterator<T>是否会产生任何额外的开销?

那么,使用Stream<T>?来破坏我的域模型是否有任何不利之处?

或者,我应该总是返回一个Iterator<T>或者Iterable<T>,并通过将该迭代器转换为StreamUtils?来让最终用户决定是否使用流.

请注意,返回a Collection不是有效选项,因为在这种情况下,大多数关系都是惰性且大小未知.

Bri*_*etz 58

这里有很多性能建议,但遗憾的是,很多都是猜测,很少有人指出真正的性能考虑因素.

@Holger 得到它的权利,指出我们应该抵制看似压倒性的倾向,让性能尾巴摇的API设计的狗.

虽然有很多考虑因素可以使得流比任何特定情况下的某些其他形式的遍历更慢,相同或更快,但是有一些因素表明流在数量上具有性能优势 - 大数据集.

还有一些额外的固定启动开销创建一个Stream比创造一个Iterator-你开始计算前几个对象.如果您的数据集很大,则无关紧要; 这是一个很小的启动成本,通过大量的计算摊销.(如果你的数据集是小,它可能也不要紧-因为如果你的程序在小数据集运行,性能一般不是你的#1关心无论是.)凡本打算平行时回事; 建立管道的任何时间都进入了Amdahl定律的连续部分; 如果你看看实现,我们努力在流设置期间保持对象倒数,但我很乐意找到减少它的方法,因为这会对并行开始赢得的盈亏平衡数据集大小产生直接影响顺序.

但是,比固定启动成本更重要的是每个元素的访问成本.在这里,溪流实际上赢了 - 并且经常赢得大奖 - 有些人可能会感到惊讶.(在我们的性能测试中,我们经常看到流管道可以胜过它们的for-loop而不是Collection同行.)而且,有一个简单的解释:Spliterator从根本上降低每个元素的访问成本Iterator,甚至是顺序.有几个原因.

  1. 迭代器协议从根本上说效率较低.它需要调用两个方法来获取每个元素.此外,因为迭代器必须对诸如next()没有调用hasNext()hasNext()多次调用之类的东西next()都很健壮,所以这两种方法通常都必须进行一些防御性编码(通常更多的有状态和分支),这会增加低效率.另一方面,即使是通过分裂器(tryAdvance)的慢速方式也没有这种负担.(对于并发数据结构来说,情况更糟,因为next/ hasNext对偶性基本上Iterator很复杂,并且实现必须做更多工作来防御并发修改而不是Spliterator实现.)

  2. Spliterator进一步提供了"快速路径"迭代 - forEachRemaining可以在大多数时间使用(减少,forEach),进一步减少了调解对数据结构内部的访问的迭代代码的开销.这也倾向于非常好地内联,这反过来又增加了其他优化的有效性,例如代码运动,边界检查消除等.

  3. 此外,遍历via Spliterator往往具有比使用更少的堆写入Iterator.使用时Iterator,每个元素都会导致一个或多个堆写入(除非Iterator可以通过转义分析对其进行标量化,并将其字段提升到寄存器中.)除其他问题外,这会导致GC卡标记活动,从而导致卡标记的缓存行争用.另一方面,Spliterators往往具有较少的状态,并且工业级forEachRemaining实现倾向于将任何内容写入堆直到遍历结束,而是将其迭代状态存储在自然映射到寄存器的本地中,从而导致内存总线活动减少.

总结:别担心,快乐. 即使没有并行性,也是Spliterator更好的Iterator.(他们通常也更容易写,更容易出错.)

  • @TagirValeev听起来像在猜测?无论如何,对我来说听起来不对.一旦我们内联终端操作,所有呼叫站点再次变为单形(由于类型锐化和每个lambda类翻译方案.) (3认同)
  • 我的猜测得到以下事实的支持:`+ PrintInlining`总是说`java.util.stream.AbstractPipeline :: evaluate(94字节)被调用者太大`,因此看起来几乎任何终端操作几乎都不能内联到调用方法. (2认同)
  • 这是一个简单的 [基准](https://gist.github.com/amaembo/04e66ecee1342bb1292c),它表明之前设置中的过滤器将简单流操作减慢了 15-35%,具体取决于 JVM 版本和任务大小。请注意,这里的拆分器是单态的(`ArraySpliterator`)。因此,我认为超态查找在这里确实很重要。你对结果有其他解释吗? (2认同)

Hol*_*ger 14

让我们比较迭代所有元素的常见操作,假设源是一个ArrayList.然后,有三种标准方法可以实现此目的:

正如您所看到的,实现代码的内部循环(这些操作最终结束)基本相同,迭代索引并直接读取数组并将元素传递给Consumer.

类似的东西适用于JRE的所有标准集合,即使您使用的是只读包装器,它们都已经适应了所有方法的实现.在后一种情况下,StreamAPI甚至会略微获胜,Collection.forEach必须在只读视图上调用才能委派给原始集合forEach.类似地,必须包装迭代器以防止尝试调用该remove()方法.相比之下,spliterator()可以直接返回原始集合,Spliterator因为它没有修改支持.因此,只读视图的流与原始集合的流完全相同.

虽然在测量实际生活性能时几乎没有注意到所有这些差异,正如所说的那样,内环,这是性能最重要的东西,在所有情况下都是相同的.

问题是从中得出哪些结论.您仍然可以将只读包装器视图返回到原始集合,因为调用者仍可以调用stream().forEach(…)以直接在原始集合的上下文中进行迭代.

由于性能并没有真正不同,您应该专注于更高级别的设计,如"我应该返回集合还是流?"中所讨论的那样.

  • @amarnathharish 我不确定你最后的评论是否有问题。对于特定任务,昂贵的组合器函数可能会导致并行处理的效率低于顺序执行的效率。请注意,对于像“collect(toList())”或“collect(joining())”这样的操作,合并的成本大致与并行处理的收益相当(在最好的情况下),因此您很少会看到任何当前实现中的“someCollection.parallelStream().collect(toList())”的优点。因此,在此示例中,您需要额外的重要中间操作才能从并行中受益。 (2认同)