最近我在學習我們產品的代碼,看到了類似以下的一段代碼:
<code>x.set
(1
) x.set
(2
) x.set
(3
) x.set
(4
) x.set
(5
)/<code>
我當時很是疑惑,為什麼不用循環呢?於是就報了一個Issue,心想這樣寫可能有它的道理,但是需要澄清一下。
另一個問題,就是我發現代碼裡對循環的使用,各有不同的方式,有人寫array.foreach(f=>_),有人用使用index的for loop,個人覺得使用foreach的代碼比較簡潔,於是我也報了Issue,看看是不是應該使用簡潔的方式來寫循環。舉例:
for loop
<code>var
index =0
var
arr =Array
[String
]var
length = arr.lengthfor
( index 0 to length ) {do
() }/<code>
for each
<code>var
index =0
var
arr =Array
[String
]var
length = arr.lengthfor
( index 0 to length ) {do
() }/<code>
明顯foreach的版本要省不少代碼。
後來和我們的工程師溝通了一下,原來我們是為了性能優化了代碼,因為for loop比foreach的性能好,所以我們採用稍微繁瑣的for loop。至於某些代碼中的foreach是因為遺留的還沒有來得及改動。
Scala的循環就行性能如何呢?我還是測試一下再說吧。
先看看不同的循環用法,我這裡測試了四種,分別是 while loop,for loop,使用range的foreach, 和使用函數的foreach
測試代碼如下:
-
<code>package
profilingobject
Loop { def whileLoop(arr:Array[Int
]):Unit
= {var
idx =0
var
n = arr.lengthval
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.lengthval
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.lengthval
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) } }/<code>
我的環境是scala 2.13.1 , 調用500000000次的結果是:
-
Bash 代碼
<code>while
loop took344
msforeach
range took484
msfor
loop took422
msforeach
function
took
719ms
/<code>
可以看出,while loop是最快的,一般形式的foreach最慢,差不多是while loop的一倍。但是如果使用range的話,foreach循環也不算太慢。
那麼為什麼foreach會慢呢? 主要是foreach的函數調用帶來了額外的開銷。我們上面看到的數據其實是編譯器已經優化後的數字,如果我們把java的hotspot編譯選項關閉,(-Xint)再看看性能。
<code>while
loop took8548
msforeach
range took39392
msfor
loop took40799
msforeach
function
took
103489ms
/<code>
如果關閉JIT,foreach的性能要遠遠差於其他幾個選項。
對於循環的性能,我們可以得出這樣的結論:
- 在正常打開JIT的情況下,foreach的性能大概比其他幾個選項慢一倍,其他幾個選項性能接近
- 在關閉JIT優化的情況下。foreach的性能要遠低於其他選項 (生產環境一般不考慮)
那麼對於開頭講的不用循環,直接重複代碼呢?我們也測試了一下:
<code>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()
+ " took " + (tEnd - tStart) + "ms")
}
def
main(args:Array[String]) {
50000000, "whileLoop")
50000000, "repeatLoop")
}
}
/<code>
經過50000000次循環,數據如下:
<code>whileLoop
took 281ms
repeatLoop
took 47ms
/<code>
確實,因為循環控制的邏輯帶來的額外開銷,比簡單的重複代碼性能下降了不少。
為了性能,你願意犧牲代碼的可維護性麼? 單選
0
人0%
是
0
人0%
否
0
人0%
看情況
好了,數據我們都有了,問題來了,為了性能考慮,你願意犧牲多少代碼的簡潔性和可讀性呢?有興趣的讀者可以參加本文中的投票,給出你的意見。
我的觀點:
- 性能很重要,但是為了性能而犧牲代碼的可讀性,可維護性,我覺得是值得考慮的,除非是項目非常關鍵的部件,我會傾向保留代碼的可維護性。
- 我們的項目是Java/Scala混編,本來用Scala就是為了它的一些先進的語法特性,主要是代碼的易讀易寫。為了性能優化,我們把Scala的代碼寫的和Java一樣或者還不如Java易讀,是否有悖我們採用Scala的初衷呢?