【问题标题】:Kotlin stdlib operatios vs for loopsKotlin 标准库操作与 for 循环
【发布时间】:2017-10-24 13:51:22
【问题描述】:

我写了以下代码:

val src = (0 until 1000000).toList()
val dest = ArrayList<Double>(src.size / 2 + 1)    

for (i in src)
{
    if (i % 2 == 0) dest.add(Math.sqrt(i.toDouble()))
}

IntellJ(在我的例子中是 AndroidStudio)询问我是否想用 stdlib 中的操作替换 for 循环。这将产生以下代码:

val src = (0 until 1000000).toList()
val dest = ArrayList<Double>(src.size / 2 + 1)
src.filter { it % 2 == 0 }
   .mapTo(dest) { Math.sqrt(it.toDouble()) }

现在我必须说,我喜欢更改后的代码。当我遇到类似情况时,我发现编写比 for 循环更容易。然而,在阅读了 filter 函数的作用后,我意识到与 for 循环相比,这是一个慢得多的代码。 filter 函数创建一个新列表,其中仅包含 src 中与谓词匹配的元素。因此,在 stdlib 版本的代码中,又创建了一个列表和一个循环。 Ofc 对于小型列表可能并不重要,但总的来说这听起来不是一个好的选择。特别是如果你应该像这样链接更多的方法,你可以获得很多额外的循环,这些循环可以通过编写一个 for 循环来避免。

我的问题是在 Kotlin 中什么被认为是好的做法。我应该坚持 for 循环还是我错过了一些东西并且它不起作用,因为我认为它起作用了。

【问题讨论】:

    标签: kotlin


    【解决方案1】:

    如果您关心性能,您需要的是Sequence。例如,您上面的代码将是

    val src = (0 until 1000000).toList()
    val dest = ArrayList<Double>(src.size / 2 + 1)
    src.asSequence()
        .filter { it % 2 == 0 }
        .mapTo(dest) { Math.sqrt(it.toDouble()) }
    

    在上面的代码中,filter返回另一个Sequence,代表一个中间步骤。还没有真正创建任何内容,没有创建对象或数组(新的Sequence 包装器除外)。只有在调用终端运算符mapTo 时,才会创建结果集合。

    如果你学过java 8 stream,你可能会觉得上面的解释有点眼熟。实际上,Sequence 大致相当于 java 8 Stream 的 kotlin。它们具有相似的目的和性能特征。唯一的区别是Sequence 不是为ForkJoinPool 设计的,因此更容易实现。

    当涉及多个步骤或集合可能很大时,建议使用Sequence 而不是普通的.filter {...}.mapTo{...}。我还建议您使用Sequence 表单而不是命令式表单,因为它更容易理解。当数据处理涉及 5 个或更多步骤时,命令式可能会变得复杂,因此难以理解。如果只有一步,你就不需要Sequence,因为它只会产生垃圾,并没有给你任何有用的东西。

    【讨论】:

    • 有道理,但是当我尝试它时,它的顺序更慢。
    • 这……太棒了。你确定你的基准是正确的吗?我的意思是,有足够的热身,没有 JIT 删除循环,重复足够的时间,所以没有边缘情况等。至少在我这边 Seqence 更快。 (大约快 25%)
    • 可能是我对编译器和 JVM 的了解不足以正确进行基准测试。我测量循环前后的时间。没有热身 - 我们在谈论什么样的热身?我的结果:sequence: 110 ms filter_map: 57 ms forEach: 48 ms for loop: 35 ms filter_map 正在使用非序列操作。
    • 有时有几个循环。即使他们复制整个集合,由于良好的参考局部性而表现出良好的性能。见:stackoverflow.com/questions/35629159/…
    • @rozina This SO post 解释了如何在 JVM 上正确地进行基准测试。尽管它旨在教育 Java 程序员,但它也适用于 kotlin,因为 kotlin 具有 JVM 后端。这是我的测试代码的要点:gist.github.com/Glease/900fe08d757631e97e230d90a9b4faa2
    【解决方案2】:

    你错过了一些东西。 :-)

    在这种特殊情况下,您可以使用IntProgression

    val progression = 0 until 1_000_000 step 2
    

    然后,您可以通过各种方式创建所需的正方形列表:

    // may make the list larger than necessary
    // its internal array is copied each time the list grows beyond its capacity
    // code is very straight forward
    progression.map { Math.sqrt(it.toDouble()) }
    
    // will make the list the exact size needed
    // no copies are made
    // code is more complicated
    progression.mapTo(ArrayList(progression.last / 2 + 1)) { Math.sqrt(it.toDouble()) }
    
    // will make the list the exact size needed
    // a single intermediate list is made
    // code is minimal and makes sense
    progression.toList().map { Math.sqrt(it.toDouble()) }
    

    【讨论】:

    • 代码只是一个虚构的例子。仍然是一个很酷的技巧 :) 虽然,创建进度不会创建一个新列表吗?可能有一个循环?
    • 不,进程在迭代时会生成它的元素,并且只存储诸如 first、last 和 step 之类的内容。
    【解决方案3】:

    我的建议是选择您喜欢的任何编码风格。 Kotlin 既是面向对象又是函数式语言,这意味着您的两个命题都是正确的。

    通常,函数式结构更倾向于可读性而不是性能;但是,在某些情况下,程序代码也将更具可读性。您应该尽量坚持使用一种样式,但如果您觉得某些代码更适合您的限制(可读性、性能或两者兼而有之),请不要害怕切换一些代码。

    【讨论】:

      【解决方案4】:

      转换后的代码不需要手动创建目标列表,可以简化为:

      val src = (0 until 1000000).toList()
      
      val dest = src.filter { it % 2 == 0 }
                    .map { Math.sqrt(it.toDouble()) }
      

      正如@glee8e 的出色回答中所提到的,您可以使用序列来进行惰性评估。使用序列的简化代码:

      val src = (0 until 1000000).toList()
      
      val dest = src.asSequence()                      // change to lazy
                    .filter { it % 2 == 0 }
                    .map { Math.sqrt(it.toDouble()) }
                    .toList()                          // create the final list
      

      请注意,在末尾添加toList() 是为了从序列更改回最终列表,该列表是在处理过程中制作的一个副本。您可以省略该步骤以保留为序列。

      @hotkey 强调 cmets 很重要,他说你不应该总是假设另一个迭代或列表的副本会导致比惰性评估更差的性能。 @hotkey 说:

      有时会有几个循环。即使他们复制整个集合,由于良好的参考局部性而表现出良好的性能。见:Kotlin's Iterable and Sequence look exactly same. Why are two types required?

      摘自该链接:

      ...在大多数情况下,它具有良好的locality of reference,因此可以利用 CPU 缓存、预测、预取等功能,因此即使是集合的多次复制仍然可以很好地工作,并且在具有小集合的简单情况下表现更好。

      @glee8e 表示 Kotlin 序列和 Java 8 流之间存在相似之处,详细比较参见:What Java 8 Stream.collect equivalents are available in the standard Kotlin library?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-02-10
        • 1970-01-01
        • 2011-01-31
        • 1970-01-01
        • 2020-07-10
        • 1970-01-01
        • 2018-01-01
        相关资源
        最近更新 更多