简述: 今天迎来了Effective Kotlin系列的第四篇文章: 使用Sequence序列来优化大集合的频繁操做.关于Sequence这个主题应该你们都不陌生,我写过几篇有关它的文章,能够说得上很详细了。若是你对它的使用不太熟悉,欢迎查看下面几篇有关文章:数组
翻译说明:app
原标题: Effective Kotlin: Use Sequence for bigger collections with more than one processing step函数
原文地址: blog.kotlin-academy.com/effective-k…工具
原文做者: Marcin Moskalapost
开发者常常会忽略了Iterable
(迭代器)与Sequence
(序列)的区别。这其实也很正常,特别是当你去比较Iterable
与Sequence
的接口定义时,它们长得几乎同样。性能
interface Iterable<out T> {
operator fun iterator(): Iterator<T>
}
interface Sequence<out T> {
operator fun iterator(): Iterator<T>
}
复制代码
对比上面代码,你只能说出它们之间惟一不同就是接口名不一样而已。可是Iterable
和Sequence
却有着彻底不一样的用法,所以它们操做集合的函数的工做原理也是彻底不一样的。测试
序列是基于惰性的工做原理,所以处理序列的中间操做函数是不进行任何计算的。相反,它们会返回上一个中间操做处理后产生的新序列。全部这些一系列中间计算都将在终端操做执行中被肯定,例如常见的终端操做toList
或count
.在另外一方面,处理Iterable
的每一个中间操做函数都是会返回一个新的集合。优化
fun main(args: Array<String>) {
val seq = sequenceOf(1,2,3)
print(seq.filter { it % 2 == 1 })
// Prints: kotlin.sequences.FilteringSequence@XXXXXXXX
print(seq.filter { it % 2 == 1 }.toList()) // Prints: [1, 3]
val list = listOf(1,2,3)
print(list.filter { it % 2 == 1 }) // Prints: [1, 3]
}
复制代码
序列的filter函数是一个中间操做,因此它不会作任何的计算,而是通过新的处理步骤对序列进行了加工。最终的计算将在终端操做中完成,如toList函数ui
正由于这样,处理操做的顺序也是不一样的。在处理序列过程当中,咱们一般会对单个元素进行一系列的总体操做,而后再对下一个元素作进行一系列的总体操做,直处处理完集合中全部元素为止。在处理iterable
过程当中,咱们是每一步操做都是针对整个集合进行,直到全部操做步骤执行完毕为止。spa
sequenceOf(1,2,3)
.filter { println("Filter $it, "); it % 2 == 1 }
.map { println("Map $it, "); it * 2 }
.toList()
// Prints: Filter 1, Map 1, Filter 2, Filter 3, Map 3,
listOf(1,2,3)
.filter { println("Filter $it, "); it % 2 == 1 }
.map { println("Map $it, "); it * 2 }
// Prints: Filter 1, Filter 2, Filter 3, Map 1, Map 3,
复制代码
因为这种惰性处理方式以及针对每一个元素进行处理的顺序,咱们能够生成一个不定长的Sequence
序列
generateSequence(1) { it + 1 }
.map { it * 2 }
.take(10)
.forEach(::print)
// Prints: 2468101214161820
复制代码
对于具备必定经验的Kotlin开发人员来讲,这应该不陌生吧,可是在大多数文章或书籍中并无说起到有关序列一个更重要的知识点: 对于处理多个单一处理步骤的集合使用序列更高效
多个处理步骤是什么意思?个人意思不只仅是多个处理集合的单一函数。因此若是你比较这两个函数:
fun singleStepListProcessing(): List<Product> {
return productsList.filter { it.bought }
}
fun singleStepSequenceProcessing(): List<Product> {
return productsList.asSequence()
.filter { it.bought }
.toList()
}
复制代码
你会注意到它们性能对比几乎没有任何差别,或者说处理简单的集合速度更快(由于它是内联的)。假如你对比了多个处理步骤的函数,好比先是filter
处理而后进行了map
处理的twoStepListProcessing
函数,那么差别将是很明显了。
fun twoStepListProcessing(): List<Double> {
return productsList
.filter { it.bought }
.map { it.price }
}
fun twoStepSequenceProcessing(): List<Double> {
return productsList.asSequence()
.filter { it.bought }
.map { it.price }
.toList()
}
fun threeStepListProcessing(): Double {
return productsList
.filter { it.bought }
.map { it.price }
.average()
}
fun threeStepSequenceProcessing(): Double {
return productsList.asSequence()
.filter { it.bought }
.map { it.price }
.average()
复制代码
差别到底有多大呢? 让咱们对比一下基准测量出来的平均操做时间吧:
twoStepListProcessing 81 095 ns/op
twoStepSequenceProcessing 55 685 ns/op
twoStepListProcessingAndAcumulate 83 307 ns/op
twoStepSequenceProcessingAndAcumulate 6 928 ns/op
复制代码
当咱们使用Sequences时, twoStepSequenceProcessing
函数明显比twoStepListProcessing
函数处理集合速度要快不少。在这种状况下,优化的效率约为30%左右。
当咱们分别使用Sequences
和Iterable
处理集合数据来得到某个具体的数值而不是得到一个新集合时,它们之间的效率差别将会变大。由于在这种状况下,序列根本就不须要建立任何中间集合。
来看一些典型的现实生活的例子,假设咱们须要计算成年人购买该产品的平均价格:
fun productsListProcessing(): Double {
return clientsList
.filter { it.adult }
.flatMap { it.products }
.filter { it.bought }
.map { it.price }
.average()
}
fun productsSequenceProcessing(): Double {
return clientsList.asSequence()
.filter { it.adult }
.flatMap { it.products.asSequence() }
.filter { it.bought }
.map { it.price }
.average()
}
复制代码
这是对比结果:
SequencesBenchmark.productsListProcessing 712 434 ns/op
SequencesBenchmark.productsSequenceProcessing 572 012 ns/op
复制代码
咱们大概提升了20%的优化效率,这比直接处理没有使用flatMap的状况要低一点,但这已是一个很大的提高了。
当你一次又一次对比测量的性能数据时,你会发现以下这个规律:
当咱们有多个处理步骤时,使用序列处理集合一般比直接处理集合更快。
在有一些不经常使用的操做中使用序列处理速度并不会更快,由于咱们须要完整地操做整个集合。来自Kotlin stdlib标准库中的sorted
就是一个明显的例子。
sorted
使用的最佳实现:它将Sequence
中的元素转换到List
中,而后使用Java stdlib标准库中的sort
函数进行排序操做。这个缺点就在于相比相同操做Collection
的过程,中间这个转换过程是须要花费额外的时间的(尽管若是Iterable
不是Collection
或数组,那么差别就并不大,由于它转换过程也是须要花费时间的)
若是Sequence序列有相似sort这样的函数是有争议的,由于它只是固定空间长度上的惰性,而且不能用于不定长的序列。之因此引进它是由于它是一种比较受欢迎的函数,而且以这种方式使用它更容易。可是Kotlin开发人员应该要记住,它不能用于不定长的序列
generateSequence(0) { it + 1 }.sorted().take(10).toList()
// 不定长的计算时间
generateSequence(0) { it + 1 }.take(10).sorted().toList()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码
sorted
是一个少见处理步骤的例子,它在Collection
上使用的效率会比Sequence
更高。当咱们使用多个处理步骤和单个排序函数是,咱们能够期待使用Sequence
序列处理的性能将获得提高。
// Took around 150 482 ns
fun productsSortAndProcessingList(): Double {
return productsList
.sortedBy { it.price }
.filter { it.bought }
.map { it.price }
.average()
}
// Took around 96 811 ns
fun productsSortAndProcessingSequence(): Double {
return productsList.asSequence()
.sortedBy { it.price }
.filter { it.bought }
.map { it.price }
.average()
}
复制代码
Java8中引入了流来处理集合。它们表现得看起来和Kotlin中的序列很像。
productsList.asSequence()
.filter { it.bought }
.map { it.price }
.average()
productsList.stream()
.filter { it.bought }
.mapToDouble { it.price }
.average()
.orElse(0.0)
复制代码
它们也都是基于惰性求值的原理而且在最后(终端)处理集合。Java中的流对于集合的处理效率几乎和Kotlin中的序列处理集合同样高。Java中的Stream流和Kotlin中的Sequence序列二者最大的差异以下所示:
Sequence
序列有更多的操做符函数(由于它们能够被定义成扩展函数)而且它们的用法也相对更简单(这是由于Kotlin的序列是已经在使用的Java流基础上设计的 - 例如咱们可使用toList()
而不是collect(Collectors.toList())
)Stream
流支持可使用parallel
函数以并行模式使用Java流. 当咱们拥有一台具备多个内核的计算机时,这能够为咱们带来巨大的性能提高。Sequence
序列可用于通用模块、Kotlin/JS模块和Kotlin/Native模块中。除此以外,当咱们不使用并行模式时,要说Java stream和 Kotlin sequence哪一个更高效,这个真的很难说。
个人建议是仅仅将Java中的Stream
用于计算量较大的处理以及须要启用并行模式的场景。不然其余通常场景使用Kotlin Stdlib标准库中Sequence
序列,能够给你带来相同效率而且操做函数使用起来也很简单,代码更加简洁。
老实说这篇文章,好的地方在于原做者把Kotlin中的序列和Java 8中的流作了一个很好的对比,以及做者给出本身的使用建议以及针对性能效率都是经过实际基准测试结果进行对比的。可是惟一美中不足的是对于Kotlin中哪一种场景下使用sequence更好,并无说的很清楚。关于这点我想补充一下:
这里只是简单总结了几点,具体详细内容可参考以前三篇有关Kotlin中序列的文章。好了第四篇完美收工。
Effective Kotlin翻译系列
原创系列:
翻译系列:
实战系列:
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~