Scala循環性能問題,為了性能,你願意犧牲代碼的可維護性麼?

Scala循環性能問題,為了性能,你願意犧牲代碼的可維護性麼?

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

<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.length

for

( index 0 to length ) {

do

() }/<code>

for each

<code>

var

index =

0

var

arr =

Array

[

String

]

var

length = arr.length

for

( index 0 to length ) {

do

() }/<code>

明顯foreach的版本要省不少代碼。

後來和我們的工程師溝通了一下,原來我們是為了性能優化了代碼,因為for loop比foreach的性能好,所以我們採用稍微繁瑣的for loop。至於某些代碼中的foreach是因為遺留的還沒有來得及改動。

Scala的循環就行性能如何呢?我還是測試一下再說吧。

先看看不同的循環用法,我這裡測試了四種,分別是 while loop,for loop,使用range的foreach, 和使用函數的foreach

測試代碼如下:

-

<code>

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) } }/<code>

我的環境是scala 2.13.1 , 調用500000000次的結果是:

-

Bash 代碼

<code>

while

loop took

344

ms

foreach

range took

484

ms

for

loop took

422

ms

foreach

function

took

719

ms

/<code>

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

那麼為什麼foreach會慢呢? 主要是foreach的函數調用帶來了額外的開銷。我們上面看到的數據其實是編譯器已經優化後的數字,如果我們把java的hotspot編譯選項關閉,(-Xint)再看看性能。

<code>

while

loop took

8548

ms

foreach

range took

39392

ms

for

loop took

40799

ms

foreach

function

took

103489

ms

/<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的初衷呢?


分享到:


相關文章: