Scala中的抽象类真的比性能更好吗?

时间:2011-02-10 20:59:13

标签: performance scala jvm

楼梯书的摘录:

  

如果效率非常重要,那就倾向于使用课程。大多数Java   运行时使类成员的虚方法调用更快   比接口方法调用。 Traits被编译为接口   因此可能会产生轻微的性能开销。但是,你应该   只有当您知道有关特征构成表演时才做出此选择   瓶颈,并有证据表明实际上使用了一个类   解决了这个问题。

我写了一些简单的代码来看看幕后真的发生了什么。我确实注意到invokevirtual用于抽象类,而invokeinterface用于接口。 但无论我写的是什么样的代码,他们总是粗略地执行相同的操作。我在服务器模式下使用HotSpot 1.6.0_18。

JIT在优化方面做得如此出色吗? 有没有人有一个示例代码来证明书中关于invokevirutal更快操作的说法?

3 个答案:

答案 0 :(得分:7)

如果HotSpot注意到调用站点上的所有实例属于同一类型,则它可以使用单态方法调用,并且虚拟和接口方法都以相同的方式进行优化。文档PerformanceTechniquesVirtualCalls不区分虚拟和接口方法。

但在一般的非单形情况下,可能会有一些差异。 InterfaceCalls文档说:

  

没有简单的前缀方案,其中接口的方法在实现该接口的每个类中的固定偏移处显示。相反,在通用(非单态)情况下,汇编编码的存根例程必须从接收器的klassOop获取已实现接口的列表,并遍历该列表以寻找当前目标接口。

它也证实了两者的单形情况相同:

  

几乎相同的优化适用于接口调用以及虚拟调用。与虚拟调用一样,大多数接口调用都是单态的,因此可以通过廉价检查呈现为直接调用。

其他JVM可能有不同的优化。

您可以尝试一个微基准测试(if you know how),它调用实现相同接口的多个类上的方法,以及扩展相同抽象类的多个类。这样就可以强制JVM使用非单态方法调用。 (虽然在现实生活中任何差异都可能无关紧要,因为大多数呼叫站点都是单形的。)

答案 1 :(得分:3)

最重要的是,您必须自己为自己的应用程序测量它,看它是否重要。您可以使用当前的JVM获得相反的违反直觉的结果。试试这个。

File TraitAbstractPackage.scala

package traitvsabstract

trait T1 { def x: Int; def inc: Unit }
trait T2 extends T1 { def x_=(x0: Int): Unit }
trait T3 extends T2 { def inc { x = x + 1 } }

abstract class C1 { def x: Int; def inc: Unit }
abstract class C2 extends C1 { def x_=(x0: Int): Unit }
abstract class C3 extends C2 { def inc { x = x + 1 } }

File TraitVsAbstract.scala

object TraitVsAbstract {
  import traitvsabstract._

  class Ta extends T3 { var x: Int = 0}
  class Tb extends T3 {
    private[this] var y: Long = 0
    def x = y.toInt
    def x_=(x0: Int) { y = x0 } 
  }
  class Tc extends T3 {
    private[this] var xHidden: Int = 0
    def x = xHidden
    def x_=(x0: Int) { if (x0 > xHidden) xHidden = x0 }
  }

  class Ca extends C3 { var x: Int = 0 }
  class Cb extends C3 {
    private[this] var y: Long = 0
    def x = y.toInt
    def x_=(x0: Int) { y = x0 } 
  }
  class Cc extends C3 {
    private[this] var xHidden: Int = 0
    def x = xHidden
    def x_=(x0: Int) { if (x0 > xHidden) xHidden = x0 }
  }

  def Tbillion3(t: T3) = {
    var i=0; while (i<1000000000) { t.inc; i+=1 }; t.x
  }

  def Tbillion1(t: T1) = {
    var i=0; while (i<1000000000) { t.inc; i+=1 }; t.x
  }

  def Cbillion3(c: C3) = {
    var i=0; while (i<1000000000) { c.inc; i+=1 }; c.x
  }

  def Cbillion1(c: C1) = {
    var i=0; while (i<1000000000) { c.inc; i+=1 }; c.x
  }

  def ptime(f: => Int) {
    val t0 = System.nanoTime
    val ans = f.toString
    val t1 = System.nanoTime
    printf("Answer: %s; elapsed: %.2f seconds\n",ans,(t1-t0)*1e-9)
  }

  def main(args: Array[String]) {
    for (i <- 1 to 3) {
      println("Iteration "+i)
      val t1s,t3s = List(new Ta, new Tb, new Tc)
      val c1s,c3s = List(new Ca, new Cb, new Cc)
      t1s.foreach(x => ptime(Tbillion1(x)))
      t3s.foreach(x => ptime(Tbillion3(x)))
      c1s.foreach(x => ptime(Cbillion1(x)))
      c3s.foreach(x => ptime(Cbillion3(x)))
      println
    }
  }
}

每个人都应该打印出1000000000作为答案,所花费的时间应该为零(如果JVM非常聪明)或大约需要增加十亿个数字。但至少在我的系统上,Sun JVM向后优化 - 重复运行变慢 - 抽象类比特性慢。 (你可能希望与java -XX:+PrintCompilation一起运行以试图弄清楚出了什么问题;我怀疑是僵尸。)

此外,值得注意的是scalac -optimise无法改善问题 - 这完全取决于JVM。

相比之下,JRockit JVM以一致的中等性能转变,但同样,特性击败了类别。由于时间是一致的,我会报告它们:类为3.35s(if语句为3.62s),所有性状为2.51秒,if-statement或no。

(我发现这种趋势一般都是正确的:Hotspot在某些情况下会产生极快的性能,而在其他情况下(比如这种情况)会让人感到困惑并且速度慢得惊人; JRockit永远不会超级快 - 不要费心去尝试甚至在基元之外获得类似C的表现 - 但它很少犯错误。)

答案 2 :(得分:0)

来自 Inside the Java Virtual Machine 的引用(调用说明和速度)

  

当Java虚拟机时   遇到 invokevirtual   指导和解决象征   参考直接引用   实例方法,即直接引用   可能是方法的偏移   表。从那时起,   可以使用相同的偏移量。为   但是, invokeinterface 指令   虚拟机必须   每次搜索方法表   单次指令   遇到了,因为它无法承担   偏移量与前一个相同   时间。