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

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

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

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

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

for loop

 
 
 
  1. var index = 0 
  2. var arr = Array[String] 
  3. var length = arr.length  
  4. for ( index <- 0 to length ) { 
  5.     do() 

for each

 
 
 
  1. var index = 0 
  2. var arr = Array[String] 
  3. var length = arr.length  
  4. for ( index <- 0 to length ) { 
  5.     do() 

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

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

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

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

测试代码如下:

 
 
 
  1. package profiling 
  2.  
  3. object Loop { 
  4.  
  5.   def whileLoop(arr:Array[Int]): Unit = { 
  6.     var idx = 0 
  7.     var n = arr.length 
  8.     val tStart = System.currentTimeMillis() 
  9.     while (idx < n) { 
  10.       arr(idx) = 1 
  11.       idx += 1 
  12.     } 
  13.     val tEnd = System.currentTimeMillis() 
  14.     println("while loop took " + (tEnd - tStart) + "ms") 
  15.   } 
  16.  
  17.   def forLoop(arr:Array[Int]): Unit = { 
  18.     var idx = 0 
  19.     var n = arr.length 
  20.     val tStart = System.currentTimeMillis() 
  21.     for(idx <- 0 until n) { 
  22.       arr(idx) = 1 
  23.     } 
  24.     val tEnd = System.currentTimeMillis() 
  25.     println("for loop took " + (tEnd - tStart) + "ms") 
  26.   } 
  27.  
  28.   def foreachLoop(arr:Array[Int]): Unit = { 
  29.     var n = arr.length 
  30.     val tStart = System.currentTimeMillis() 
  31.     (0 until n).foreach{idx => arr(idx) = 1} 
  32.     val tEnd = System.currentTimeMillis() 
  33.     println("foreach range took " + (tEnd - tStart) + "ms") 
  34.   } 
  35.  
  36.   def foreachFuncLoop(arr:Array[Int]): Unit = { 
  37.     val tStart = System.currentTimeMillis() 
  38.     arr.foreach{ idx => arr(idx) = 1} 
  39.     val tEnd = System.currentTimeMillis() 
  40.     println("foreach function took " + (tEnd - tStart) + "ms") 
  41.   } 
  42.  
  43.   def profileRun(n: Int) { 
  44.     val arr = new Array[Int](n) 
  45.  
  46.     whileLoop(arr) 
  47.     foreachLoop(arr) 
  48.     forLoop(arr) 
  49.     foreachFuncLoop(arr) 
  50.   } 
  51.  
  52.   def main(args:Array[String]) { 
  53.     profileRun(args(0).toInt) 
  54.   } 

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

Bash 代码

 
 
 
  1. while loop took 344ms 
  2. foreach range took 484ms 
  3. for loop took 422ms 
  4. foreach function took 719ms 

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

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

 
 
 
  1. while loop took 8548ms 
  2. foreach range took 39392ms 
  3. for loop took 40799ms 
  4. foreach function took 103489ms 

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

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

  • 在正常打开JIT的情况下,foreach的性能大概比其他几个选项慢一倍,其他几个选项性能接近
  • 在关闭JIT优化的情况下。foreach的性能要远低于其他选项 (生产环境一般不考虑)

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

 
 
 
  1. package profiling 
  2.  
  3. object Loop2Repeat { 
  4.   def whileLoop(): Unit = { 
  5.     var idx = 0 
  6.     var n = 5 
  7.     var x = 0 
  8.     while (idx < n) { 
  9.       x = idx 
  10.       idx += 1 
  11.     } 
  12.   } 
  13.  
  14.   def repeatLoop(): Unit = { 
  15.     var x = 0 
  16.     x = 1 
  17.     x = 2 
  18.     x = 3 
  19.     x = 4 
  20.     x = 5 
  21.   } 
  22.  
  23.   def test( f:()=>Unit, num: Int, name: String): Unit = { 
  24.     val tStart = System.currentTimeMillis() 
  25.     ( 0 until num).foreach{ _ => f} 
  26.     val tEnd = System.currentTimeMillis() 
  27.     println(name + " took " + (tEnd - tStart) + "ms") 
  28.   } 
  29.  
  30.   def main(args:Array[String]) { 
  31.     test(whileLoop, 50000000, "whileLoop") 
  32.     test(repeatLoop, 50000000, "repeatLoop") 
  33.   } 
  34.  

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

 
 
 
  1. whileLoop took 281ms 
  2. repeatLoop took 47ms 

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

分享标题:Scala循环性能问题,为了性能,你愿意牺牲代码的可维护性么?
链接URL:http://www.shufengxianlan.com/qtweb/news47/341797.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联