Kotlin 科特林:在这个例子中,为什么序列更有效?

Kotlin 科特林:在这个例子中,为什么序列更有效?,kotlin,Kotlin,目前,我正在研究Kotlin,对序列与集合的比较有一个问题 我阅读了一篇关于此主题的文章,在那里您可以找到以下代码片段: 列表实现: val list = generateSequence(1) { it + 1 } .take(50_000_000) .toList() measure { list .filter { it % 3 == 0 } .average() } // 8644 ms val sequence = gener

目前,我正在研究Kotlin,对序列与集合的比较有一个问题

我阅读了一篇关于此主题的文章,在那里您可以找到以下代码片段:

列表实现:

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
序列实现:

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
这里的要点是序列实现大约快10倍

然而,我真的不明白这是为什么。我知道,对于一个序列,您可以进行“惰性评估”,但我找不到任何原因来解释为什么这有助于减少本例中的处理

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

val result = sequenceOf("a", "b", "c")
    .map {
        println("map: $it")
        it.toUpperCase()
    }
    .any {
        println("any: $it")
        it.startsWith("B")
    }
因为对于序列,您“垂直”处理数据,当第一个元素以“B”开头时,您不必映射其余元素。这在这里是有道理的


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

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

此外,本文中使用的基准测试方法也不是非常精确。试着用英语重复这个实验

初始代码生成一个包含50000个对象的列表:

 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…)派生新序列:

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

.average()

一些相关阅读:


让我们看看这两个实现实际上在做什么:

  • 列表实现首先在内存中创建一个包含5000万个元素的
    List
    。这将需要最少200MB的内存,因为一个整数需要4个字节

    (事实上,它可能远不止这些。正如Alexey Romanov指出的,由于它是一个通用的
    List
    实现,而不是
    IntList
    ,所以它不会直接存储整数,而是将它们“装箱”——存储对
    Int
    对象的引用。在JVM上,每个引用可以是8或16字节,each
    Int
    可能需要16,即1–2GB。此外,根据
    列表的创建方式,它可能从一个小数组开始,随着列表的增长,不断创建越来越大的数组,每次复制所有值,仍然使用更多内存。)

    然后它必须从列表中读取所有值,过滤它们,并在内存中创建另一个列表

    最后,它必须再次读取所有那些值,以计算平均值

  • 另一方面,序列实现不需要存储任何东西!它只需按顺序生成值,并在执行每个值时检查它是否可以被3整除,如果可以,则将其包含在平均值中

    (如果你是“手工”实现的,你会这么做。)

  • 您可以看到,除了可分性检查和平均计算之外,列表实现还进行了大量的内存访问,这将花费大量时间。这是它比序列版本慢得多的主要原因,序列版本不会

    看到这一点,你可能会问为什么我们不在所有地方都使用序列……但这是一个相当极端的例子。设置并迭代序列本身有一些开销,对于小列表,可能会超过内存开销。因此,序列只有在列表非常大的情况下才有明显的优势按照顺序,有几个中间步骤,和/或许多项在过程中被过滤掉(特别是如果序列是无限的!)


    根据我的经验,这些情况并不经常发生。但这个问题表明,当它们发生时,识别它们是多么重要!

    好吧,与OP中的第二个示例相比,在使用序列或正常集合时,过滤然后计算平均值所需的操作数大致相同。那么,原因是什么更高的性能是因为不必创建中间集合,对吗?@Aliquis操作的数量不同-在第一个示例中,您需要迭代50_000_000次以创建筛选列表,然后继续计算平均值(这涉及更多迭代).在第二个例子中,您只需直接计算平均值(在应用过滤的情况下迭代序列一次),而不创建任何冗余对象+可能避免对intsGood answer进行自动装箱,但我要补充两件事:1.序列也要慢得多(如果可用的话)如果您不需要按顺序处理每个元素。2.初始列表将远远大于200MB,因为需要对整数进行装箱,请参阅。@AlexeyRomanov:很好,谢谢!我应该发现整数将被装箱。我将更新我的答案。