Groovy/Scala/Java под капотом

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

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

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

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

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

Ответ 1

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

Pimp My Library

  /**
   * 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

Структурные типы (aka Duck Typing)

  /**
   * 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

Ответ 2

Много хороших ответов, я постараюсь добавить что-то еще, что я получил от вашего вопроса. Нет обертывания объектов 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 в конструкторе, например, где это не вызовет проблем с производительностью.

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

Ответ 3

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

Ответ 4

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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