源自Stream.emits()

时间:2017-10-24 14:10:28

标签: scala fs2

我刚遇到一个问题,即使用要通过text.utf8encode写入文件的字符串流来降低fs2性能。我试图改变我的源代码以使用分块字符串来提高性能,但观察到的是性能下降。

据我所知,它归结为以下内容:在源自flatMap的流上调用Stream.emits()可能非常昂贵。基于传递给Stream.emits()的序列的大小,时间使用似乎是指数的。下面的代码片段显示了一个示例:

/*
    Test done with scala 2.11.11 and fs2 version 0.10.0-M7.
 */

val rangeSize = 20000
val integers = (1 to rangeSize).toVector

// Note that the last flatMaps are just added to show extreme load for streamA.
val streamA = Stream.emits(integers).flatMap(Stream.emit(_))
val streamB = Stream.range(1, rangeSize + 1).flatMap(Stream.emit(_))

streamA.toVector  // Uses approx. 25 seconds (!)
streamB.toVector  // Uses approx. 15 milliseconds

这是一个错误,还是应该避免使用Stream.emits()来处理大型序列?

1 个答案:

答案 0 :(得分:1)

TLDR:分配。

更长的答案:

有趣的问题。我分别在两种方法上运行了JFR配置文件,并查看了结果。立即引起我注意的第一件事是分配的数量。

Stream.emit

Stream.emit allocations

Stream.range

Stream.range allocations

我们可以看到Stream.emit分配了大量Append个实例,这是Catenable[A]的具体实现,Stream.emit中使用的类型是private[fs2] final case class Append[A](left: Catenable[A], right: Catenable[A]) extends Catenable[A] 折叠的类型:

Catenable[A]

这实际上来自foldLeft实施foldLeft(empty: Catenable[B])((acc, a) => acc :+ f(a))

的实施方式
:+

Append为每个元素分配一个新的Append对象。这意味着我们至少会生成20000个这样的Stream.range个对象。

/** * Lazily produce the range `[start, stopExclusive)`. If you want to produce * the sequence in one chunk, instead of lazily, use * `emits(start until stopExclusive)`. * * @example {{{ * scala> Stream.range(10, 20, 2).toList * res0: List[Int] = List(10, 12, 14, 16, 18) * }}} */ def range(start: Int, stopExclusive: Int, by: Int = 1): Stream[Pure,Int] = unfold(start){i => if ((by > 0 && i < stopExclusive && start < stopExclusive) || (by < 0 && i > stopExclusive && start > stopExclusive)) Some((i, i + by)) else None } 的文档中还有关于如何生成单块而不是将流进一步划分的提示,如果这是一个很大的范围我们可能会很糟糕&# 39;重新生成:

Stream.emits

你可以看到这里没有额外的包装,只有作为范围的一部分发出的整数。另一方面,Append为序列中的每个元素创建一个left对象,其中我们有right包含流的尾部,fs2包含当前元素价值我们在。

这是一个错误吗?我会说不,但我肯定会将此作为性能问题打开amount库维护者。