2011-02-10 50 views
13

从楼梯一本书的摘录:斯卡拉的抽象类真的比特性表现更好吗?

如果效率是非常重要的,对精益使用类。与接口方法调用相比,大多数Java运行时对类成员的虚拟方法调用是更快速的操作 。性状被编译到接口 ,因此可能会支付一些性能开销。但是,只有当您知道该特征构成性能 瓶颈并且有证据表明使用班级实际上 解决了问题时,您才应该做出此选择。

我写了一些简单的代码,看看幕后发生了什么。在抽象类中我注意到了invokevirtual,在接口的情况下我注意到了invokeinterface。 但是无论我写什么样的代码,他们总是大致执行相同的操作。我在服务器模式下使用HotSpot 1.6.0_18。

JIT是否做了这么好的优化工作? 有没有人有一个示例代码,证明这本书有关invokevirutal是更快的操作索赔?

+0

如果您正在认真考虑这种低级优化,那么您正在使用错误的编程语言。 – Raphael 2011-02-10 21:37:48

+4

@Raphael如果您在Android系统或类似系统上使用Scala,您可能需要注意所有那些您通常不会重复考虑的事情。 – wheaties 2011-02-10 21:54:59

回答

7

如果HotSpot注意到呼叫站点的所有实例都是相同类型的,它可以使用单形方法调用,并且虚拟和接口方法都以相同的方式优化。文档PerformanceTechniquesVirtualCalls没有区分虚拟方法和接口方法。

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

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

这也证实了单态的情况下是两个相同的:

几乎相同优化适用于接口的调用,以虚拟呼叫。与虚拟呼叫一样,大多数界面调用都是单形的,因此可以通过便宜的支票以直接调用方式呈现。

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

您可以尝试一个微型基准测试(if you know how),它调用多个实现相同接口的类的方法,以及多个扩展相同抽象类的类。这样,应该可以强制JVM使用非单形方法调用。 (虽然在现实生活中可能没有什么区别,因为大多数呼叫站点都是单形的。)

0

Inside the Java Virtual Machine(调用指令和速度)引述:

当Java虚拟机 遇到invokevirtual 指令和解决符号 参照直接引用的 实例方法,即直接引用 很可能是表格中的方法 的偏移量。从这一点开始,可以使用相同的偏移量。对于 invokeinterface指令,然而, 虚拟机将不得不通过方法表 搜索 遇到的指令每次 单的时候,因为它不能承担 偏移量是一样的前面 时间。

3

底线是,你将不得不为自己的应用程序自己测量它,看它是否重要。您可以通过当前的JVM获得相当违反直觉的结果。尝试一下。

文件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 } } 

文件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 
    } 
    } 
} 

每个人都应该打印出10亿的答案,而且所花费的时间应该是零(如果JVM是很聪明)或约需要添加十亿个数字。但至少在我的系统中,Sun JVM向后优化 - 重复运行速度变慢 - 而抽象类比特性慢。 (你可能想用java -XX:+PrintCompilation来试图弄清楚出了什么问题;我怀疑是僵尸)。

另外,值得注意的是scalac -optimise没有任何改进的地方 - 这一切都取决于JVM。

相比之下,JRockit JVM的性能始终如一,但同样,特质击败了类。由于时间是一致的,我会报告他们:3.35s的类(3.62s为一个if语句)与2.51秒的所有特性,if语句或否。 (我发现这种趋势总体上是真实的:在某些情况下,热点产生快速的性能,而在其他情况下(例如这种情况),会变得困惑并且非常慢; JRockit从不超速 - 不要打扰试图从原语中获得类似C的性能 - 但它很少出现错误)。