Scala循环性能问题,为了性能,你愿意牺牲代码的可维护性么?

最近我在学习我们产品的代码,看到了一段代码,我当时很是疑惑:为什么不用循环呢?于是就报了一个Issu
首页 新闻资讯 行业资讯 Scala循环性能问题,为了性能,你愿意牺牲代码的可维护性么?

最近我在学习我们产品的代码,看到了类似以下的一段代码:

复制

x.set(1)  x.set(2)  x.set(3) x.set(4) x.set(5)
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

我当时很是疑惑,为什么不用循环呢?于是就报了一个Issue,心想这样写可能有它的道理,但是需要澄清一下。

[[323560]]

另一个问题,就是我发现代码里对循环的使用,各有不同的方式,有人写array.foreach(f=>_),有人用使用index的for  loop,个人觉得使用foreach的代码比较简洁,于是我也报了Issue,看看是不是应该使用简洁的方式来写循环。举例:

for loop

复制

var index = 0 var arr = Array[String] var length = arr.length  for ( index <- 0 to length ) {     do() }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

for each

复制

var index = 0 var arr = Array[String] var length = arr.length  for ( index <- 0 to length ) {     do() }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

明显foreach的版本要省不少代码。

后来和我们的工程师沟通了一下,原来我们是为了性能优化了代码,因为for loop比foreach的性能好,所以我们采用稍微繁琐的for  loop。至于某些代码中的foreach是因为遗留的还没有来得及改动。

Scala的循环就行性能如何呢?我还是测试一下再说吧。

先看看不同的循环用法,我这里测试了四种,分别是 while loop,for loop,使用range的foreach,  和使用函数的foreach。

测试代码如下:

复制

package profiling  object Loop {    def whileLoop(arr:Array[Int]): Unit = {     var idx = 0     var n = arr.length     val tStart = System.currentTimeMillis()     while (idx < n) {       arr(idx) = 1       idx += 1     }     val tEnd = System.currentTimeMillis()     println("while loop took " + (tEnd - tStart) + "ms")   }    def forLoop(arr:Array[Int]): Unit = {     var idx = 0     var n = arr.length     val tStart = System.currentTimeMillis()     for(idx <- 0 until n) {       arr(idx) = 1     }     val tEnd = System.currentTimeMillis()     println("for loop took " + (tEnd - tStart) + "ms")   }    def foreachLoop(arr:Array[Int]): Unit = {     var n = arr.length     val tStart = System.currentTimeMillis()     (0 until n).foreach{idx => arr(idx) = 1}     val tEnd = System.currentTimeMillis()     println("foreach range took " + (tEnd - tStart) + "ms")   }    def foreachFuncLoop(arr:Array[Int]): Unit = {     val tStart = System.currentTimeMillis()     arr.foreach{ idx => arr(idx) = 1}     val tEnd = System.currentTimeMillis()     println("foreach function took " + (tEnd - tStart) + "ms")   }    def profileRun(n: Int) {     val arr = new Array[Int](n)      whileLoop(arr)     foreachLoop(arr)     forLoop(arr)     foreachFuncLoop(arr)   }    def main(args:Array[String]) {     profileRun(args(0).toInt)   } }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

  • 52.

  • 53.

  • 54.

  • 55.

我的环境是scala 2.13.1 , 调用500000000次的结果是:

Bash 代码

复制

while loop took 344ms foreach range took 484ms for loop took 422ms foreach function took 719ms
  • 1.

  • 2.

  • 3.

  • 4.

可以看出,while loop是最快的,一般形式的foreach最慢,差不多是while  loop的一倍。但是如果使用range的话,foreach循环也不算太慢。

那么为什么foreach会慢呢?  主要是foreach的函数调用带来了额外的开销。我们上面看到的数据其实是编译器已经优化后的数字,如果我们把java的hotspot编译选项关闭,(-Xint)再看看性能。

复制

while loop took 8548ms foreach range took 39392ms for loop took 40799ms foreach function took 103489ms
  • 1.

  • 2.

  • 3.

  • 4.

如果关闭JIT,foreach的性能要远远差于其他几个选项。

对于循环的性能,我们可以得出这样的结论:

  • 在正常打开JIT的情况下,foreach的性能大概比其他几个选项慢一倍,其他几个选项性能接近

  • 在关闭JIT优化的情况下。foreach的性能要远低于其他选项 (生产环境一般不考虑)

那么对于开头讲的不用循环,直接重复代码呢?我们也测试了一下:

复制

package profiling  object Loop2Repeat {   def whileLoop(): Unit = {     var idx = 0     var n = 5     var x = 0     while (idx < n) {       x = idx       idx += 1     }   }    def repeatLoop(): Unit = {     var x = 0     x = 1     x = 2     x = 3     x = 4     x = 5   }    def test( f:()=>Unit, num: Int, name: String): Unit = {     val tStart = System.currentTimeMillis()     ( 0 until num).foreach{ _ => f}     val tEnd = System.currentTimeMillis()     println(name + " took " + (tEnd - tStart) + "ms")   }    def main(args:Array[String]) {     test(whileLoop, 50000000, "whileLoop")     test(repeatLoop, 50000000, "repeatLoop")   }  }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

经过50000000次循环,数据如下:

复制

whileLoop took 281ms repeatLoop took 47ms
  • 1.

  • 2.

确实,因为循环控制的逻辑带来的额外开销,比简单的重复代码性能下降了不少。

 

32    2020-04-24 10:44:45    Scala 代码 开发