4 votes

Groovy, Scala et Java sous le capot

J'ai utilisé Java pendant 6-7 ans, mais il y a quelques mois, j'ai découvert Groovy et j'ai commencé à économiser beaucoup de temps de frappe... Ensuite, je me suis demandé comment certaines choses fonctionnaient sous le capot (parce que les performances de groovy sont vraiment médiocres) et j'ai compris que pour vous donner dactylographie dynamique cada Groovy est un objet MetaClass qui gère toutes les choses que la JVM ne peut pas gérer elle-même. Bien sûr, cela introduit une couche intermédiaire entre ce que vous écrivez et ce que vous exécutez, ce qui ralentit tout.

Il y a quelques jours, j'ai commencé à recevoir des informations sur les sujets suivants Scala . Comment ces deux langues se comparent-elles dans leurs traductions du code d'octets ? Combien de choses ajoutent-ils à la structure normale que l'on obtiendrait par un code Java simple ?

Je veux dire.., Scala es statique typée , de sorte qu'une enveloppe de Java devraient être plus légères, puisque beaucoup de choses sont vérifiées au moment de la compilation, mais je ne suis pas sûr des différences réelles de ce qui se passe à l'intérieur. (Je ne parle pas de l'aspect fonctionnel des classes de Scala par rapport aux autres, c'est différent)

Quelqu'un peut-il m'éclairer ?

De Commentaires de SyntaxT3rr0r il semble que la seule façon d'obtenir moins de typage et la même performance soit d'écrire un traducteur intermédiaire qui traduise quelque chose en Java (en laissant javac le compiler) sans modifier la façon dont les choses sont exécutées, en ajoutant simplement du sucre syntaxique sans se soucier des autres retombées du langage lui-même.

23voto

retronym Points 35066

Scala fait de plus en plus d'efforts pour réduire le coût de l'abstraction.

Dans les commentaires en ligne dans le code, j'explique les caractéristiques de performance de l'accès aux tableaux, des types pimpés, des types structurels et de l'abstraction sur les primitives et les objets.

Tableaux

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

Types structurels (ou typage canard)

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

Spécialisation

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

Clôtures et compréhensions For

En Scala for se traduit par une chaîne d'appels à des fonctions d'ordre supérieur : foreach , map , flatMap y withFilter . C'est très puissant, mais vous devez être conscient que le code suivant n'est pas aussi efficace qu'une construction similaire en Java. Scala 2.8 @spécialisera Function1 pour au moins Double y Int et, espérons-le, @specialize Traversable#foreach ce qui permettra au moins de supprimer le coût de la mise en boîte.

Le corps de la for-compréhension est transmis sous la forme d'une fermeture, qui est compilée dans une classe interne anonyme.

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

L ligne 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

10voto

Daniel C. Sobral Points 159554

Beaucoup de bonnes réponses, je vais essayer d'ajouter quelque chose d'autre que j'ai tiré de votre question. Il y a non l'emballage des objets Scala. Par exemple, les deux classes suivantes, respectivement en Scala et en Java, génèrent exactement le même bytecode :

// 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;
  }
}

Il convient de noter que Scala accède toujours aux champs par l'intermédiaire de getters et de setters, même sur d'autres méthodes de la même classe. L'essentiel, cependant, est qu'il n'y a absolument pas d'enveloppement de classe ici. C'est la même chose, qu'il soit compilé en Java ou en Scala.

Aujourd'hui, Scala le rend plus facile d'écrire un code plus lent. En voici quelques exemples :

  • Le système for sont notablement plus lents que Java lorsqu'il s'agit simplement d'incrémenter des indices -- la solution, jusqu'à présent, est d'utiliser while à la place, bien que quelqu'un ait écrit un plugin pour le compilateur qui fait cette conversion automatiquement. Tôt ou tard, une telle optimisation sera ajoutée.

  • Il est très facile d'écrire des fermetures et de passer des fonctions en Scala. Cela rend le code beaucoup plus lisible, mais c'est nettement plus lent que pas le faire en boucles serrées.

  • Il est également facile de paramétrer les fonctions de manière à ce que l'on puisse passer Int ce qui peut entraîner de mauvaises performances si vous manipulez des primitives (en Scala, AnyVal sous-classes).

Voici un exemple d'une classe écrite en Scala de deux manières différentes, où la plus compacte est environ deux fois plus lente :

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)
}

C'est aussi un bon exemple qui montre qu'il est tout à fait possible d'équilibrer les performances lorsque c'est nécessaire. La version la plus rapide utilise foreach dans le constructeur, par exemple, où il ne causera pas de problèmes de performance.

En fin de compte, tout est une question de perspective. L'appel de méthodes sur des objets est plus lent que l'appel direct de fonctions et de procédures, et c'était une objection majeure à la programmation orientée objet, mais cela s'est avéré ne pas être un gros problème la plupart du temps.

9voto

Michael Borgwardt Points 181658

Une chose à savoir : Java 7 introduira un nouveau dynamique invoqué pour la JVM, ce qui rendra inutile une grande partie de la "magie des métaclasses" de Groovy et devrait accélérer considérablement les implémentations de langages dynamiques sur la JVM.

6voto

David Crawshaw Points 4842

Vous pouvez translittérer Java en Scala et obtenir un bytecode qui est presque exactement le même. Scala est donc parfaitement capable d'être aussi rapide que Java.

Cela dit, il existe de nombreuses façons d'écrire un code Scala plus lent et plus gourmand en mémoire qui soit plus court et plus lisible que son équivalent en Java. Et c'est tant mieux ! Nous utilisons Java, et non C, parce que la protection de la mémoire améliore notre code. L'expressivité supplémentaire de Scala permet d'écrire des programmes plus courts et donc moins bogués qu'en Java. Parfois, cela nuit aux performances, mais la plupart du temps, ce n'est pas le cas.

6voto

Rex Kerr Points 94401

Retronym et David ont couvert les points principaux concernant Scala : il est essentiellement aussi rapide que Java, et il en est ainsi parce qu'il est typiquement statique (ne nécessitant donc pas de vérifications supplémentaires au moment de l'exécution) et qu'il utilise des enveloppes légères que la JVM peut généralement supprimer entièrement.

Scala permet d'utiliser très facilement de puissantes fonctionnalités de bibliothèques génériques. Comme pour toute bibliothèque générique puissante en Java, il en résulte une certaine pénalité en termes de performances. Par exemple, l'utilisation d'un java.util.HashMap pour implémenter une carte entre des octets et des octets sera douloureusement lente en Java (comparée à une table de consultation de tableau primitif), et le sera tout autant en Scala. Mais Scala vous offre beaucoup plus de fonctionnalités de ce type, et les rend étonnamment faciles à invoquer, au point que vous pouvez vraiment demander à ce qu'une quantité étonnante de travail soit effectuée avec très peu de code. Comme toujours, lorsqu'il est facile de demander beaucoup, les gens demandent parfois beaucoup, et se demandent ensuite pourquoi cela prend tant de temps. (Et la facilité avec laquelle on demande rend les choses encore plus surprenantes lorsqu'on apprend (ou qu'on réfléchit attentivement) à ce qui doit se passer en coulisses).

La seule critique légitime que l'on pourrait formuler est que Scala ne facilite pas autant qu'il le pourrait l'écriture de code à haute performance ; la plupart des caractéristiques de facilité d'utilisation sont orientées vers la programmation fonctionnelle générique, qui reste assez rapide, mais pas autant que l'accès direct aux types primitifs. Par exemple, Scala dispose d'un outil incroyablement puissant, le for mais elle utilise des types génériques, donc les primitives doivent être encadrées, et donc vous ne pouvez pas l'utiliser efficacement pour itérer sur des tableaux primitifs ; vous devez utiliser une boucle while à la place. (La différence de performance est susceptible de diminuer dans la version 2.8 avec les spécialisations mentionnées par retronym).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X