Kotlin:为什么在此示例中Sequence更高效?

时间:2018-11-10 10:01:25

标签: kotlin

目前,我正在研究Kotlin,并对序列与馆藏有疑问。

我读了一个有关该主题的blog post,在那里您可以找到以下代码片段:

列表实现:

val list = generateSequence(1) { it + 1 }
    .take(50_000_000)
    .toList()

measure {
    list
        .filter { it % 3 == 0 }
        .average()
}
// 8644 ms

序列实施:

val sequence = generateSequence(1) { it + 1 }
    .take(50_000_000)

measure {
    sequence
        .filter { it % 3 == 0 }
        .average()
}
// 822 ms

这里的意思是Sequence实现的速度快了大约10倍。

但是,我不太了解为什么。我知道使用Sequence可以进行“惰性评估”,但是在此示例中,我找不到任何有助于减少处理量的原因。

但是,我在这里知道为什么序列通常更快:

val result = sequenceOf("a", "b", "c")
    .map {
        println("map: $it")
        it.toUpperCase()
    }
    .any {
        println("any: $it")
        it.startsWith("B")
    }

由于使用序列可以“垂直”处理数据,因此当第一个元素以“ B”开头时,不必为其余元素进行映射。在这里很有意义。

那么,为什么在第一个示例中它也更快?

2 个答案:

答案 0 :(得分:6)

让我们看看这两种实现的实际作用:

  1. List实现首先在具有5000万个元素的内存中创建一个List。这将至少占用200MB的空间,因为整数需要4个字节。

    (实际上,它可能远不止于此。<< Alexey Romanov指出,由于它是通用的List实现而不是IntList,因此不会可以直接存储整数,但可以将它们``装箱''-存储对Int对象的引用。在JVM上,每个引用可以是8或16个字节,每个Int可以占用16个字节,从而得到1 –2GB。此外,取决于创建List的方式,它可能从一个小的数组开始,并随着列表的增长而不断创建越来越大的数组,每次都复制所有值,仍然使用更多的内存。)< / p>

    然后,它必须从列表中读取所有值,对其进行过滤,然后在内存中创建另一个列表。

    最后,它必须重新读回所有那些值,以计算平均值。

  2. 另一方面,
  3. Sequence实现不需要存储任何内容!它只是简单地按顺序生成值,然后像每个值一样检查它是否可以被3整除,如果可以,则将其包括在平均值中。

    (如果您是“手动”实施,这几乎就是您要做的。)

您可以看到,除了进行除数检查和平均计算外, List实现还进行了大量的内存访问,这将花费大量时间。比序列版本慢,没有!

看到这一点,您可能会问为什么我们不在所有地方都使用Sequences……但这是一个极端的例子。设置然后迭代Sequence本身会有一些开销,对于较小的列表,这些开销可能超过内存开销。因此,只有在列表很大,严格按顺序处理,有几个中间步骤和/或沿途过滤掉许多项的情况下(尤其是序列是无限的!),序列才具有明显的优势。 / p>

根据我的经验,这些情况很少发生。但是这个问题表明,在他们识别出它们时,识别它们有多么重要!

答案 1 :(得分:3)

利用惰性评估可以避免创建与最终目标无关的中间对象。

此外,上述文章中使用的基准测试方法也不是非常准确。尝试使用JMH重复该实验。

初始代码产生一个包含50_000_000个对象的列表:

 val list = generateSequence(1) { it + 1 }
  .take(50_000_000)
  .toList()

然后遍历它,并创建另一个包含其元素子集的列表

.filter { it % 3 == 0 }

...然后继续计算平均值:

.average()

使用序列可以避免执行所有这些中间步骤。下面的代码不会产生50_000_000个元素,而只是表示1 ... 50_000_000个序列:

val sequence = generateSequence(1) { it + 1 }
  .take(50_000_000)

在其中添加过滤不会触发计算本身,而是从现有的序列(3、6、9 ...)中得出新的序列:

.filter { it % 3 == 0 }

最后,调用终端操作,该操作触发对序列的评估和实际计算:

.average()

一些相关的读物:

Kotlin: Beware of Java Stream API Habits

Kotlin Collections API Performance Antipatterns