Scala循環(huán)性能問題,為了性能,你愿意犧牲代碼的可維護性么?
最近我在學習我們產(chǎn)品的代碼,看到了類似以下的一段代碼:
- x.set(1)
- x.set(2)
- x.set(3)
- x.set(4)
- x.set(5)
我當時很是疑惑,為什么不用循環(huán)呢?于是就報了一個Issue,心想這樣寫可能有它的道理,但是需要澄清一下。
另一個問題,就是我發(fā)現(xiàn)代碼里對循環(huán)的使用,各有不同的方式,有人寫array.foreach(f=>_),有人用使用index的for loop,個人覺得使用foreach的代碼比較簡潔,于是我也報了Issue,看看是不是應該使用簡潔的方式來寫循環(huán)。舉例:
for loop
- var index = 0
- var arr = Array[String]
- var length = arr.length
- for ( index <- 0 to length ) {
- do()
- }
for each
- var index = 0
- var arr = Array[String]
- var length = arr.length
- for ( index <- 0 to length ) {
- do()
- }
明顯foreach的版本要省不少代碼。
后來和我們的工程師溝通了一下,原來我們是為了性能優(yōu)化了代碼,因為for loop比foreach的性能好,所以我們采用稍微繁瑣的for loop。至于某些代碼中的foreach是因為遺留的還沒有來得及改動。
Scala的循環(huán)就行性能如何呢?我還是測試一下再說吧。
先看看不同的循環(huán)用法,我這里測試了四種,分別是 while loop,for loop,使用range的foreach, 和使用函數(shù)的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)
- }
- }
我的環(huán)境是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循環(huán)也不算太慢。
那么為什么foreach會慢呢? 主要是foreach的函數(shù)調用帶來了額外的開銷。我們上面看到的數(shù)據(jù)其實是編譯器已經(jīng)優(yōu)化后的數(shù)字,如果我們把java的hotspot編譯選項關閉,(-Xint)再看看性能。
- while loop took 8548ms
- foreach range took 39392ms
- for loop took 40799ms
- foreach function took 103489ms
如果關閉JIT,foreach的性能要遠遠差于其他幾個選項。
對于循環(huán)的性能,我們可以得出這樣的結論:
- 在正常打開JIT的情況下,foreach的性能大概比其他幾個選項慢一倍,其他幾個選項性能接近
- 在關閉JIT優(yōu)化的情況下。foreach的性能要遠低于其他選項 (生產(chǎn)環(huán)境一般不考慮)
那么對于開頭講的不用循環(huán),直接重復代碼呢?我們也測試了一下:
- 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")
- }
- }
經(jīng)過50000000次循環(huán),數(shù)據(jù)如下:
- whileLoop took 281ms
- repeatLoop took 47ms
確實,因為循環(huán)控制的邏輯帶來的額外開銷,比簡單的重復代碼性能下降了不少。