Groovy / Scala / Java под капотом - PullRequest
10 голосов
/ 27 марта 2010

Я использовал Java около 6-7 лет, затем несколько месяцев назад я обнаружил Groovy и начал экономить много времени на наборе текста. Затем я удивился, как работают некоторые вещи капот (потому что производительность Groovy действительно плохая) и понимал, что дать вам динамическую типизацию каждый Groovy объект - это MetaClass объект, который обрабатывает все вещи, которые JVM не могла справиться сам. Конечно, это вводит слой посередине между тем, что вы пишете, и тем, что вы выполняете, что замедляет все.

Затем, однажды, я начал получать информацию о Scala . Как эти два языка сравниваются в своих переводах байт-кода? Сколько вещей они добавляют к нормальной структуре, что бы получить простой Java-код?

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

Может ли кто-нибудь просветить меня?

Из комментария WizardOfOdds кажется, что единственный способ получить меньше печатания и одинаковую производительность - это написать промежуточный переводчик, который что-то переводит в Java код (позволяющий javac-код его скомпилировать), не изменяя, как это происходит. выполнено, просто добавив к нему синтаксический сахар, не заботясь о других ошибках самого языка.

Ответы [ 6 ]

23 голосов
/ 27 марта 2010

Scala делает все возможное, чтобы сократить расходы на абстракцию.

В комментариях, встроенных в код, я объясняю характеристики производительности доступа к массиву, скрытых типов, структурных типов и абстрагирования от примитивов и объектов.

Массивы

object test {
  /**
   * From the perspective of the Scala Language, there isn't a distinction between
   * objects, primitives, and arrays. They are all unified under a single type system,
   * with Any as the top type.
   *
   * Array access, from a language perspective, looks like a.apply(0), or a.update(0, 1)
   * But this is compiled to efficient bytecode without method calls. 
   */
  def accessPrimitiveArray {
    val a = Array.fill[Int](2, 2)(1)
    a(0)(1) = a(1)(0)        
  }
  // 0: getstatic #62; //Field scala/Array$.MODULE$:Lscala/Array$;
  // 3: iconst_2
  // 4: iconst_2
  // 5: new #64; //class test$$anonfun$1
  // 8: dup
  // 9: invokespecial #65; //Method test$$anonfun$1."<init>":()V
  // 12:  getstatic #70; //Field scala/reflect/Manifest$.MODULE$:Lscala/reflect/Manifest$;
  // 15:  invokevirtual #74; //Method scala/reflect/Manifest$.Int:()Lscala/reflect/AnyValManifest;
  // 18:  invokevirtual #78; //Method scala/Array$.fill:(IILscala/Function0;Lscala/reflect/ClassManifest;)[Ljava/lang/Object;
  // 21:  checkcast #80; //class "[[I"
  // 24:  astore_1
  // 25:  aload_1
  // 26:  iconst_0
  // 27:  aaload
  // 28:  iconst_1
  // 29:  aload_1
  // 30:  iconst_1
  // 31:  aaload
  // 32:  iconst_0
  // 33:  iaload
  // 34:  iastore
  // 35:  return

Прокачай мою библиотеку

  /**
   * Rather than dynamically adding methods to a meta-class, Scala
   * allows values to be implicity converted. The conversion is
   * fixed at compilation time. At runtime, there is an overhead to
   * instantiate RichAny before foo is called. HotSpot may be able to
   * eliminate this overhead, and future versions of Scala may do so
   * in the compiler.
   */
  def callPimpedMethod {    
    class RichAny(a: Any) {
      def foo = 0
    }
    implicit def ToRichAny(a: Any) = new RichAny(a)
    new {}.foo
  }
  // 0: aload_0
  //   1: new #85; //class test$$anon$1
  //   4: dup
  //   5: invokespecial #86; //Method test$$anon$1."<init>":()V
  //   8: invokespecial #90; //Method ToRichAny$1:(Ljava/lang/Object;)Ltest$RichAny$1;
  //   11:  invokevirtual #96; //Method test$RichAny$1.foo:()I
  //   14:  pop
  //   15:  return

Структурные типы (ака утка)

  /**
   * Scala allows 'Structural Types', which let you have a compiler-checked version
   * of 'Duck Typing'. In Scala 2.7, the invocation of .size was done with reflection.
   * In 2.8, the Method object is looked up on first invocation, and cached for later
   * invocations..
   */
  def duckType {
    val al = new java.util.ArrayList[AnyRef]
    (al: { def size(): Int }).size()
  }
  // [snip]
  // 13:  invokevirtual #106; //Method java/lang/Object.getClass:()Ljava/lang/Class;
  // 16:  invokestatic  #108; //Method reflMethod$Method1:(Ljava/lang/Class;)Ljava/lang/reflect/Method;
  // 19:  aload_2
  // 20:  iconst_0
  // 21:  anewarray #102; //class java/lang/Object
  // 24:  invokevirtual #114; //Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
  // 27:  astore_3
  // 28:  aload_3
  // 29:  checkcast #116; //class java/lang/Integer

Специализация

  /**
   * Scala 2.8 introduces annotation driven specialization of methods and classes. This avoids
   * boxing of primitives, at the cost of increased code size. It is planned to specialize some classes
   * in the standard library, notable Function1.
   *
   * The type parameter T in echoSpecialized is annotated to instruct the compiler to generated a specialized version
   * for T = Int.
   */
  def callEcho {    
    echo(1)
    echoSpecialized(1)
  }
  // public void callEcho();
  //   Code:
  //    Stack=2, Locals=1, Args_size=1
  //    0:   aload_0
  //    1:   iconst_1
  //    2:   invokestatic    #134; //Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
  //    5:   invokevirtual   #138; //Method echo:(Ljava/lang/Object;)Ljava/lang/Object;
  //    8:   pop
  //    9:   aload_0
  //    10:  iconst_1
  //    11:  invokevirtual   #142; //Method echoSpecialized$mIc$sp:(I)I
  //    14:  pop
  //    15:  return


  def echo[T](t: T): T = t
  def echoSpecialized[@specialized("Int") T](t: T): T = t
}

Закрытия и для понимания

В Scala for переводится в цепочку вызовов функций более высокого порядка: foreach, map, flatMap и withFilter. Это действительно мощно, но вы должны знать, что следующий код не так эффективен, как похожая конструкция в Java. Scala 2.8 будет @specialize Function1 как минимум на Double и Int, и, надеюсь, также @specialize Traversable#foreach, что, по крайней мере, снимет стоимость бокса.

Тело для понимания передается как замыкание, которое компилируется в анонимный внутренний класс.

def simpleForLoop {
  var x = 0
  for (i <- 0 until 10) x + i
}
// public final int apply(int);   
// 0:   aload_0
// 1:   getfield    #18; //Field x$1:Lscala/runtime/IntRef;
// 4:   getfield    #24; //Field scala/runtime/IntRef.elem:I
// 7:   iload_1
// 8:   iadd
// 9:   ireturn


// public final java.lang.Object apply(java.lang.Object);

// 0:   aload_0
// 1:   aload_1
// 2:   invokestatic    #35; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
// 5:   invokevirtual   #37; //Method apply:(I)I
// 8:   invokestatic    #41; //Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
// 11:  areturn

// public test$$anonfun$simpleForLoop$1(scala.runtime.IntRef);
// 0:   aload_0
// 1:   aload_1
// 2:   putfield    #18; //Field x$1:Lscala/runtime/IntRef;
// 5:   aload_0
// 6:   invokespecial   #49; //Method scala/runtime/AbstractFunction1."<init>":()V
// 9:   return

LineNumberTable: строка 4: 0

// 0:   new #16; //class scala/runtime/IntRef
// 3:   dup
// 4:   iconst_0
// 5:   invokespecial   #20; //Method scala/runtime/IntRef."<init>":(I)V
// 8:   astore_1
// 9:   getstatic   #25; //Field scala/Predef$.MODULE$:Lscala/Predef$;
// 12:  iconst_0
// 13:  invokevirtual   #29; //Method scala/Predef$.intWrapper:(I)Lscala/runtime/RichInt;
// 16:  ldc #30; //int 10
// 18:  invokevirtual   #36; //Method scala/runtime/RichInt.until:(I)Lscala/collection/immutable/Range$ByOne;
// 21:  new #38; //class test$$anonfun$simpleForLoop$1
// 24:  dup
// 25:  aload_1
// 26:  invokespecial   #41; //Method test$$anonfun$simpleForLoop$1."<init>":(Lscala/runtime/IntRef;)V
// 29:  invokeinterface #47,  2; //InterfaceMethod scala/collection/immutable/Range$ByOne.foreach:(Lscala/Function1;)V
// 34:  return
10 голосов
/ 28 марта 2010

Множество хороших ответов, я постараюсь добавить что-то еще, что я получил от вашего вопроса. Существует нет упаковки объектов Scala. Например, следующие два класса в Scala и Java соответственно генерируют абсолютно одинаковый байт-код:

// This is Scala
class Counter {
  private var x = 0
  def getCount() = {
    val y = x
    x += 1
    y
  }
}

// This is Java
class Counter {
  private int x = 0;

  private int x() {
    return x;
  }

  private void x_$eq(int x) {
    this.x = x;
  }

  public int getCounter() {
    int y = x();
    x_$eq(x() + 1);
    return y;
  }
}

Особо следует отметить тот факт, что Scala всегда отправляет поля через геттеры и сеттеры, даже при использовании других методов того же класса. Дело, однако, в том, что здесь не происходит абсолютно никакого переноса классов. Это одно и то же, независимо от того, скомпилирован ли он в Java или Scala.

Теперь Scala облегчает написание более медленного кода. Некоторые примеры этого:

  • Scala for заметно медленнее, чем Java, когда просто увеличивает индексы - пока что решение состоит в том, чтобы вместо этого использовать циклы while, хотя кто-то написал плагин компилятора, который выполняет это преобразование автоматически. Рано или поздно такая оптимизация будет добавлена.

  • В Scala очень легко писать замыкания и передавать функции. Это делает код намного более читабельным, но он заметно медленнее, чем , а не , делающий это в тесных циклах.

  • Также легко параметризовать функции, чтобы можно было передавать Int, что может привести к плохой производительности, если вы обрабатываете примитивы (в Scala, AnyVal подклассы).

Вот пример класса, написанного на Scala двумя различными способами, где более компактный класс примерно вдвое медленнее:

class Hamming extends Iterator[BigInt] {
  import scala.collection.mutable.Queue
  val qs = Seq.fill(3)(new Queue[BigInt])
  def enqueue(n: BigInt) = qs zip Seq(2, 3, 5) foreach { case (q, m) => q enqueue n * m }
  def next = {
    val n = qs map (_.head) min;
    qs foreach { q => if (q.head == n) q.dequeue }
    enqueue(n)
    n
  }
  def hasNext = true
  qs foreach (_ enqueue 1)
}

class Hamming extends Iterator[BigInt] {
  import scala.collection.mutable.Queue
  val q2 = new Queue[BigInt]
  val q3 = new Queue[BigInt]
  val q5 = new Queue[BigInt]
  def enqueue(n: BigInt) = {
    q2 enqueue n * 2
    q3 enqueue n * 3
    q5 enqueue n * 5
  }
  def next = {
    val n = q2.head min q3.head min q5.head
    if (q2.head == n) q2.dequeue
    if (q3.head == n) q3.dequeue
    if (q5.head == n) q5.dequeue
    enqueue(n)
    n
  }
  def hasNext = true
  List(q2, q3, q5) foreach (_ enqueue 1)
}

Это также хороший пример того, как можно идеально сбалансировать производительность при необходимости. Более быстрая версия использует, например, foreach в конструкторе, где это не вызовет проблем с производительностью.

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

9 голосов
/ 27 марта 2010

Следует помнить одну вещь: Java 7 представит новый invokedynamic байт-код для JVM, который сделает ненужной большую часть "магии метаклассов" в Groovy и должна значительно ускорить реализацию динамических языков на JVM.

6 голосов
/ 27 марта 2010

retronym и David рассмотрели основные моменты, касающиеся Scala: он по сути такой же быстрый, как и Java, и именно так происходит, поскольку он статически типизирован (поэтому не требует дополнительных проверок во время выполнения) и использует легкие обертки, которые JVM обычно может удалить полностью.

Scala позволяет легко использовать мощные универсальные библиотечные функции. Как и с любой мощной универсальной библиотекой в ​​Java, с этим связано некоторое снижение производительности. Например, использование java.util.HashMap для реализации отображения между байтами и байтами в Java будет мучительно медленным (по сравнению с таблицей поиска примитивного массива), и в Scala оно будет таким же медленным. Но Scala предоставляет вам гораздо больше функций такого рода и позволяет удивительно легко их вызывать, и вы действительно можете запросить огромный объем работы, выполняемой в очень небольшом количестве кода. Как всегда, когда вы легко просите много, люди иногда спрашивают много, а потом удивляются, почему это так долго. (А легкость спрашивать делает еще более удивительным, когда кто-то узнает (или тщательно обдумывает), что должно происходить за кулисами.)

Единственная законная критика, которую можно поднять, заключается в том, что Scala не делает настолько легким, насколько это возможно, писать высокопроизводительный код; Большинство функций простоты использования направлены на общее функциональное программирование, которое все еще довольно быстрое, но не такое быстрое, как прямой доступ к примитивным типам. Например, в Scala есть невероятно мощный цикл for, но он использует универсальные типы, поэтому примитивы должны быть упакованы, и, следовательно, вы не можете эффективно использовать их для перебора примитивных массивов; вместо этого вы должны использовать цикл while. (Разница в производительности, вероятно, уменьшится в 2,8 со специализациями, как упоминалось в ретрониме.)

6 голосов
/ 27 марта 2010

Вы можете транслитерировать Java на Scala и в итоге получить байт-код, который практически одинаков. Так что Scala вполне может быть такой же быстрой, как Java.

Тем не менее, существует множество способов написания более медленного и более интенсивного кода Scala, которые короче и более читаемы, чем эквивалент Java. И это хорошо! Мы используем Java, а не C, потому что защита памяти улучшает наш код. Особая выразительность Scala означает, что вы можете писать программы, которые короче, чем, следовательно, менее шумные, чем в Java. Иногда это ухудшает производительность, но в большинстве случаев это не так.

1 голос
/ 14 апреля 2010

Другие ответы сосредоточены на специфике скалы. Я хотел бы добавить несколько пунктов для общего случая. Прежде всего, вполне возможно написать генератор байт-кода, который генерирует код, подобный javac, но на языке, который не является java. Это становится сложнее, так как семантика языка отличается от семантики Java. Однако явная типизация не является частью семантики, а только синтаксиса (и имеет свойства обнаружения ошибок).

Производительность снижается в случае, если типы не могут быть определены статически (во время компиляции) или если язык динамический по своей природе (типизация динамическая, как во многих языках сценариев, таких как javascript, jython, jruby и т. Д.) В этих случаях с 1.6 jdk вам нужно выполнить диспетчеризацию на основе отражения. Это очевидно медленнее и не может быть легко оптимизировано горячей точкой / виртуальной машиной. Jdk 1.7 расширяет invokedynamic, так что он может фактически использоваться для динамического вызова функции, что поддерживается языками сценариев.

Компилятор javac не выполняет столько оптимизаций (jvm делает их во время выполнения), поэтому язык java довольно просто отображается в байт-код java. Это означает, что языки с одинаковой семантикой имеют преимущество по сравнению с языками с другой семантикой. Это недостаток JVM и место, где CLR (.net runtime) и LLVM имеют явные преимущества.

...