1. 程式人生 > 其它 >Scala迴圈效能問題,為了效能,你願意犧牲程式碼的可維護性麼?

Scala迴圈效能問題,為了效能,你願意犧牲程式碼的可維護性麼?

  最近我在學習我們產品的程式碼,看到了類似以下的一段程式碼:

  x.set(1)

  x.set(2)

  x.set(3)

  x.set(4)

  x.set(5)

  我當時很是疑惑,為什麼不用迴圈呢?於是就報了一個Issue,心想這樣寫可能有它的道理,但是需要澄清一下。

  另一個問題,就是我發現程式碼裡對迴圈的使用,各有不同的方式,有人寫array.foreach(f=>_),有人用使用index的for loop,個人覺得使用foreach的程式碼比較簡潔,於是我也報了Issue,看看是不是應該使用簡潔的方式來寫迴圈。舉例:

  for loop

  var index=0

  var arr=Array[String]

  var length=arr.length

  for ( index

  do()

  }

  for each

  var index=0

  var arr=Array[String]

  var length=arr.length

  for ( index

  do()

  }

  明顯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

  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)

  }

  }

  我的環境是scala 2.13.1 , 呼叫500000000次的結果是:

  -

  Bash 程式碼

  while loop took 344ms

  foreach range took 484ms

  for loop took 422ms

  foreach function took 719ms

  可以看出,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

  如果關閉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")

  }

  }

  經過50000000次迴圈,資料如下:

  whileLoop took 281ms

  repeatLoop took 47ms

  確實,因為迴圈控制的邏輯帶來的額外開銷,比簡單的重複程式碼效能下降了不少。

  為了效能,你願意犧牲程式碼的可維護性麼?