Search code examples
javascalagroovybytecode

Groovy, Scala, and Java under the hood


I used Java for like 6-7 years, but then some months ago I discovered Groovy and started to save a lot of typing... Then I wondered how certain things worked under the hood (because groovy performance is really poor) and understood that to give you dynamic typing every Groovy object is a MetaClass object that handles all the things that the JVM couldn't handle by itself. Of course this introduces a layer in the middle between what you write and what you execute that slows down everything.

Then some days ago I started getting some information about Scala. How do these two languages compare in their byte code translations? How many things do they add to the normal structure that it would be obtained by plain Java code?

I mean, Scala is static typed, so a wrapper of Java classes should be lighter, since many things are checked during compile time, but I'm not sure about the real differences of what's going inside. (I'm not talking about the functional aspect of Scala compared to the other ones, that's a different thing)

Can someone enlighten me?

From SyntaxT3rr0r's comments, it seems like that the only way to get less typing and the same performance would be to write an intermediate translator that translates something in Java code (letting javac compile it) without altering how things are executed, just adding syntactic sugar without caring about other fallbacks of the language itself.


Solution

  • Scala is doing an increasing good job of reducing the cost of abstraction.

    In the comments inline in the code, I explain the performance characteristics of Array access, pimped types, Structural Types, and abstracting over primitives and objects.

    Arrays

    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
    

    Structural Types (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
    

    Specialisation

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

    Closures and For Comprehensions

    In Scala for is translated to a chain of calls to higher order functions: foreach, map, flatMap and withFilter. This is really powerful, but you need to be aware that the following code isn't nearly as efficient as a similar looking construct in Java. Scala 2.8 will @specialize Function1 for at least Double and Int, and hopefully will also @specialize Traversable#foreach, which will at least remove the boxing cost.

    The body of the for-comprehension is passed as a closure, which is compiled to an anonymous inner class.

    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: line 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