为什么对这个Scala代码的微小更改会对性能产生如此巨大的影响?

57

我在一台32位的Debian 6.0 (Squeeze)系统上运行(一个2.5GHz的Core 2 CPU),安装了 sun-java6 6.24-1,但使用来自 Wheezy 的 Scala 2.8.1 软件包。

这段代码使用 scalac -optimise 编译后,运行需要超过30秒:

object Performance {

  import scala.annotation.tailrec

  @tailrec def gcd(x:Int,y:Int):Int = {
    if (x == 0)
      y 
    else 
      gcd(y%x,x)
  }

  val p = 1009
  val q = 3643
  val t = (p-1)*(q-1)

  val es = (2 until t).filter(gcd(_,t) == 1)
  def main(args:Array[String]) {
    println(es.length)
  }
}

但是,如果我对代码进行微小的更改,将val es=移动到main的范围内并向下移动一行,则可以在仅需1秒的时间内运行,这与等效的C ++的性能相当。有趣的是,将val es=保留在原处,但使用lazy进行限定也具有同样的加速效果。

这里发生了什么?为什么在函数范围外执行计算会慢得多?


1
我在2.8.1版本中看到了同样的问题(Sun Java 1.6.0_24-b07),没有使用“-optimize”。 - overthink
提到优化有点误导,抱歉;我从来没有看到使用它(或不使用)对我所处理的任何Scala代码的性能产生任何显着影响。 - timday
1
有趣的是...在一个类似的系统上,使用Scala 2.9.0时,无论是否使用-optimize,都会发生相同的事情。 - Kim Stebel
2个回答

53

JVM 对静态初始化器(也就是这里的内容)做的优化没有对方法调用做的那么高效。不幸的是,当你在那里做很多工作时,这会影响性能,这正是一个完美的例子。这也是为什么旧的 Application 特质被认为有问题的原因之一,也是为什么 Scala 2.9 中有一个 DelayedInit 特质,它可以得到编译器的一些帮助,将一些初始化器中的内容移动到稍后调用的方法中。


(编辑:已将“构造函数”更正为“初始化器”。这是一个相当长的笔误!)


2
这是Scala特有的还是普遍适用的?因为我不明白为什么JIT不会优化普通Java代码中的长静态初始化器(或者更准确地说,为什么它会区分静态代码块和普通代码块)。 - Voo
1
@Voo,这通常是正确的,甚至影响Java静态代码。看起来这是JVM的设计;一个可能的假设是:由于静态初始化程序只运行一次,JVM作者不想花费太多时间优化它们的用例。 - notnoop
1
@timday - 在每个昂贵的计算中都添加“lazy”后,过一段时间会变得乏味。有一个机制可以一次性完成所有操作是很好的。 - Rex Kerr
1
@notnoop 我不明白为什么。当然,静态初始化程序没有被优化 - 就像只被调用一次的任何主方法一样不会被优化。但是,如果我们内部有一些复杂的计算,为什么不按照通常的规则进行JIT编译呢? - Voo
2
@notnoop。另一方面,静态初始化程序实际上只能运行一次(每个ClassLoader),并且具有非平凡的语义:请参阅JLS 12.4.2。 JVM设计者基本上决定这不值得努力。 另外,另一个需要大量优化才能使代码更快的主要方法。 此外,main方法就像任何其他静态方法一样,实际上可以运行多次(您实际上可以自己调用主方法..令人震惊!)。 - notnoop
显示剩余3条评论

41

在顶层对象块内部的代码将被转换为该对象类上的静态初始化程序。在Java中的等效操作是

class Performance{
    static{
      //expensive calculation
    }
    public static void main(String[] args){
      //use result of expensive calculation
    }
}

HotSpot JVM在静态初始化期间遇到的代码不会执行任何优化,这是出于合理的启发式原则,因为这样的代码只会运行一次。


1
是的,这很有趣。我以一个上次在90年代中期接触Java的人的视角来看待它,但Scala作为比Java更具吸引力的语言(至少对我而言)吸引了我回到JVM。因此,当像这样的底层技术的现实意外地被揭示时,我确实会感到惊讶。 - timday
4
特别是在不可变性方面,我怀疑在类的初始化阶段进行大量计算是一种常见的模式。 - ziggystar
4
这特别影响“静态”初始化器。 - Rex Kerr
5
由于它仅影响静态初始化程序,因此它仅影响Scala单例对象,而不是类对象。 - Dave Griffith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接