【问题标题】:Why is iterating through flattened iterator slow?为什么通过扁平迭代器进行迭代很慢?
【发布时间】:2018-05-12 14:09:27
【问题描述】:
Scala 2.11.8

我正在通过扁平化和非扁平化迭代器测量迭代。我写了以下基准:

@State(Scope.Benchmark)
class SerializeBenchmark
  var list = List(
    List("test", 12, 34, 56),
    List("test-test-test", 123, 444, 0),
    List("test-test-test-tes", 145, 443, 4333),
    List("testdsfg-test-test-tes", 3145, 435, 333),
    List("test-tessdfgsdt-tessdfgt-tes", 1455, 43, 333),
    List("tesewrt-test-tessdgdsft-tes", 13345, 4533, 3222333),
    List("ewrtes6yhgfrtyt-test-test-tes", 122245, 433444, 322233),
    List("tserfest-test-testtryfgd-tes", 143345, 43, 3122233),
    List("test-reteytest-test-tes", 1121145, 4343, 3331212),
    List("test-test-ertyeu6test-tes", 14115, 4343, 33433),
    List("test-lknlkkn;lkntest-ertyeu6test-tes", 98141115, 4343, 33433),
    List("tkknknest-test-ertyeu6test-tes", 914111215, 488343, 33433),
    List("test-test-ertyeu6test-tes", 1411125, 437743, 93433),
    List("test-test-ertyeu6testo;kn;lkn;lk-tes", 14111215, 5409343, 39823),
    List("telnlkkn;lnih98st-test-ertyeu6test-tes", 1557215, 498343, 3377433)
  )

  @Benchmark
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  @BenchmarkMode(Array(Mode.AverageTime))
  def flattenerd(bh: Blackhole): Any = {
    list.iterator.flatten.foreach(bh.consume)
  }

  @Benchmark
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  @BenchmarkMode(Array(Mode.AverageTime))
  def raw(bh: Blackhole): Any = {
    list.iterator.foreach(_.foreach(bh.consume))
  }
}

多次运行这些基准测试后,我得到了以下结果:

Benchmark                      Mode  Cnt      Score      Error  Units
SerializeBenchmark.flattenerd  avgt    5  10311,373 ± 1189,448  ns/op
SerializeBenchmark.raw         avgt    5   3463,902 ±  141,145  ns/op

性能相差近 3 倍。我制作的源list 越大,性能差异就越大。为什么?

我预计会有一些性能差异,但不是 3 倍。

【问题讨论】:

  • flattenerd 不要欺负人。书呆子是人,就像你一样。 ;)
  • 嗯...Iterator[A] (scala-lang.org/api/2.12.0/scala/collection/Iterator.html) 没有flatten 方法。这意味着范围内的隐式之一是将其提升到提供 flatten(可能包括很多 GC)的东西,然后返回到 Iterator
  • 另外...不要使用 5 次迭代。尝试像 1000 来获得平均值

标签: performance scala collections jmh


【解决方案1】:

我重新运行了您的测试,在 hs_gc 配置文件下运行了更多迭代。

这些是结果:

[info] Benchmark                                                       Mode  Cnt        Score          Error  Units
[info] IteratorFlatten.flattenerd                                      avgt   50        0.708 â–’        0.120  us/op
[info] IteratorFlatten.flattenerd:â•–sun.gc.collector.0.invocations    avgt   50        8.840 â–’        2.259      ?
[info] IteratorFlatten.raw                                             avgt   50        0.367 â–’        0.014  us/op
[info] IteratorFlatten.raw:â•–sun.gc.collector.0.invocations           avgt   50        0                     ?

IteratorFlatten.flattenerd 在测试运行期间平均有 8 个 GC 周期,而 raw 有 0 个。这意味着由于 FlattenOps 分配产生的噪音(包装类及其方法,尤其是 @ 987654326@ 为每个列表分配一个迭代器),这是在Iterator 上提供flatten 方法所需要的,我们在运行时间方面受到影响。

如果我重新运行测试并将最小堆大小设置为 2G,结果会更接近:

[info] Benchmark                   Mode   Cnt        Score           Error  Units
[info] IteratorFlatten.flattenerd  avgt   50        0.615 â–’         0.041  us/op
[info] IteratorFlatten.raw         avgt   50        0.434 â–’         0.064  us/op

它的要点是,你分配的越多,GC 要做的工作就越多,暂停越多,执行越慢。

请注意,这类微基准非常脆弱,可能会产生不同的结果。确保衡量足够的分配,以使统计数据变得重要。

【讨论】:

  • 你确定来源是FlattenOps的分配?即使它没有被优化掉,与list.iterator 调用相比,它也只会增加 2 倍的 GC 负载。我认为关键的区别在于flatten 必须为每个内部列表分配内部iterator,而foreach 则不需要。因此,将flattenlist.iterator.foreach(_.iterator.foreach(bh.consume)) 与额外的内部_.iterator 调用进行比较可能会很有趣
  • @SergGr 当我的意思是FlattenOps 我真正的意思是Iterator 的整个包装器,而不仅仅是类的分配(它不是一个值类,我不认为它是优化了)。我会把钱放在:def hasNext: Boolean = it.hasNext || its.hasNext && { it = its.next().toIterator; hasNext },它为每个内部列表分配一个迭代器。
  • Yuval,这正是我在评论中的意思,这就是为什么我建议使用额外的 _.iterator 内部调用进行额外测试。我相信,如果您对差异主要来自 GC 的诊断是正确的,那么您的答案的当前措辞具有误导性。应该是“noise generated by the allocation by(或inside)FlattenOps”而不是“noise generated by the allocation的 FlattenOps"
  • @SergGr 谢谢你的评论。我已经更新了答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-07
  • 2020-11-07
  • 1970-01-01
  • 1970-01-01
  • 2019-07-30
  • 2016-10-13
相关资源
最近更新 更多