Действительно ли абстрактные классы в Scala работают лучше, чем черты?

Отрывок из книги лестницы:

Если эффективность очень важна, опирайтесь на использование класса. Большинство Java время выполнения делает вызов виртуального метода членом класса более быстрой операцией чем вызов метода интерфейса. Черты компилируются в интерфейсы и, следовательно, могут заплатить небольшие накладные расходы. Однако вы должны сделайте этот выбор только в том случае, если вы знаете, что эта характеристика представляет собой результат узкое место и имеют доказательства того, что вместо этого вместо этого использовать класс решает проблему.

Я написал простой код, чтобы увидеть, что действительно происходит за кулисами. И я заметил, что invokevirtual используется в случае абстрактного класса и invokeinterface в случае интерфейса. Но независимо от того, какой код я написал, они всегда грубо исполняли то же самое. Я использую HotSpot 1.6.0_18 в режиме сервера.

Является ли JIT такой оптимистичной работой? У кого-нибудь есть пример кода, который доказывает, что требование из книги о invokevirutal является более быстрой операцией?

Ответы

Ответ 1

Если HotSpot отмечает, что все экземпляры на сайте вызова одного типа, он может использовать вызов мономорфного метода, и оба метода виртуального и интерфейса оптимизируются одинаково. Документы PerformanceTechniques и VirtualCalls make нет различия между виртуальными и интерфейсами.

Но в общем немономорфном случае может быть какая-то разница. В документе InterfaceCalls говорится:

Нет простой схемы префикса, в которой методы интерфейса отображаются при фиксированных смещениях в каждом классе, который реализует этот интерфейс. Вместо этого, в общем (немономорфном) случае, сборка-закодированная процедура-заглушка должна извлекать список реализованных интерфейсов из приемника klassOop и прокладывать этот список в поисках текущего целевого интерфейса.

Это также подтверждает, что мономорфный случай одинаковый для обоих:

Почти одинаковые оптимизации применяются к вызовам интерфейса как к виртуальным вызовам. Как и в случае с виртуальными вызовами, большинство вызовов интерфейса мономорфны и поэтому могут отображаться как прямые вызовы с дешевой проверкой.

Другие JVM могут иметь разные оптимизации.

Вы можете попробовать микро-тест (если знаете, как), который вызывает методы на нескольких классах, реализующих один и тот же интерфейс, и на нескольких классах, которые расширяют те же абстрактные класс. Таким образом, должно быть возможно заставить JVM использовать вызовы немономорфных методов. (Хотя в реальной жизни любая разница не может иметь значения, так как большинство сайтов вызовов в любом случае мономорфны.)

Ответ 2

Суть в том, что вам придется измерять его самостоятельно для своего приложения, чтобы узнать, важно ли это. Вы можете получить довольно противоречивые результаты с текущей 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
    }
  }
}

Каждый должен распечатать 1000000000 в качестве ответа, а время, которое должно быть равно нулю (если JVM действительно умный) или примерно столько, сколько потребуется, чтобы добавить миллиард чисел. Но, по крайней мере, в моей системе Sun JVM оптимизируется в обратном направлении - повторные запуски становятся медленнее - и абстрактные классы медленнее, чем черты. (Возможно, вы захотите запустить с помощью java -XX:+PrintCompilation, чтобы попытаться выяснить, что пойдет не так, я подозреваю зомби.)

Кроме того, стоит отметить, что scalac -optimise не делает ничего, чтобы улучшить ситуацию - все это до JVM.

JVM JRKIT в отличие от этого превращается в стабильную среднюю производительность, но опять же, черты бить классы. Поскольку сроки согласованы, я сообщу им: 3.35s для классов (3.62s для одного с оператором if) против 2.51 секунды для всех признаков, if-statement или no.

(Я считаю, что эта тенденция в целом верна: в некоторых случаях Hotspot производит быстрые быстродействия, а в других (например, в этом случае) запутывается и очень медленно, JRockit никогда не бывает супер-быстрым - не пытайтесь попробовать чтобы получить C-подобную производительность даже из примитивов, но это редко приводит к ошибкам.)

Ответ 3

Цитата из Внутри виртуальной машины Java (Invocation Instructions and Speed):

Когда виртуальная машина Java сталкивается с invokevirtualинструкции и разрешает символическую ссылка на прямую ссылку на метод экземпляра, что прямая ссылка вероятно, является смещением в методе Таблица. С этого момента такое же смещение может быть использовано. Для команда invokeinterface, однако, виртуальной машине придется поиск по таблице методов каждый один раз инструкция столкнулся, потому что он не может предположить смещение совпадает с предыдущим время.