Kotlin 科特林:在这个例子中,为什么序列更有效?
目前,我正在研究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
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()
一些相关阅读:
让我们看看这两个实现实际上在做什么:
List
。这将需要最少200MB的内存,因为一个整数需要4个字节
(事实上,它可能远不止这些。正如Alexey Romanov指出的,由于它是一个通用的List
实现,而不是IntList
,所以它不会直接存储整数,而是将它们“装箱”——存储对Int
对象的引用。在JVM上,每个引用可以是8或16字节,eachInt
可能需要16,即1–2GB。此外,根据列表的创建方式,它可能从一个小数组开始,随着列表的增长,不断创建越来越大的数组,每次复制所有值,仍然使用更多内存。)
然后它必须从列表中读取所有值,过滤它们,并在内存中创建另一个列表
最后,它必须再次读取所有那些值,以计算平均值
根据我的经验,这些情况并不经常发生。但这个问题表明,当它们发生时,识别它们是多么重要!好吧,与OP中的第二个示例相比,在使用序列或正常集合时,过滤然后计算平均值所需的操作数大致相同。那么,原因是什么更高的性能是因为不必创建中间集合,对吗?@Aliquis操作的数量不同-在第一个示例中,您需要迭代50_000_000次以创建筛选列表,然后继续计算平均值(这涉及更多迭代).在第二个例子中,您只需直接计算平均值(在应用过滤的情况下迭代序列一次),而不创建任何冗余对象+可能避免对intsGood answer进行自动装箱,但我要补充两件事:1.序列也要慢得多(如果可用的话)如果您不需要按顺序处理每个元素。2.初始列表将远远大于200MB,因为需要对整数进行装箱,请参阅。@AlexeyRomanov:很好,谢谢!我应该发现整数将被装箱。我将更新我的答案。