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


分享到:


相關文章: