首页 文章

引擎盖下的Groovy / Scala / Java

提问于
浏览
10

我用 Java 就像6-7岁,然后几个月前我发现 Groovy 并开始节省大量的打字..然后我想知道某些事情是如何在幕后工作的(因为常规的表现真的很差)而且明白要给予动态输入每个 Groovy 对象都是一个 MetaClass 对象,它处理JVM无法自行处理的所有事情 . 当然,这会在您编写的内容和执行的内容之间引入一层,从而减慢所有内容 .

然后有一天我开始得到关于 Scala 的一些信息 . 这两种语言在字节码翻译中的比较如何?他们添加到普通Java代码可以获得的正常结构中有多少东西?

我的意思是, Scala 是静态类型的,因此 Java 类的包装应该更轻,因为在编译期间会检查很多东西,但我会进入内部 . (我不是在谈论 Scala 的功能方面与其他方面相比,这是另一回事)

有人可以开导我吗?

从WizardOfOdds评论看来,获得更少打字和相同性能的唯一方法是编写一个中间翻译器,翻译 Java 代码中的内容(让javac编译它)而不改变事情的执行方式,只需添加同义词糖即可 . 语言本身的其他后备 .

6 回答

  • 9

    Scala在降低抽象成本方面做得越来越好 .

    在代码中的内联注释中,我解释了数组访问,pimped类型,结构类型以及对基元和对象进行抽象的性能特征 .

    数组

    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 被转换为对更高阶函数的调用链: foreachmapflatMapwithFilter . 这非常强大,但您需要注意以下代码与Java中的类似外观构造的效率差不多 . Scala 2.8将@specialize Function1至少 DoubleInt ,并希望@specialize Traversable#foreach ,这将至少消除拳击成本 .

    for-comprehension的主体作为闭包传递,它被编译为匿名内部类 .

    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
    
  • 23

    很多好的答案,我会尝试添加我从你的问题中得到的其他东西 . 没有包装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总是通过getter和setter进入字段,甚至是同一类的其他方法 . 然而,重点是这里绝对没有类包装 . 无论是用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)
    }
    

    这也是在需要时如何完全 balancer 性能的一个很好的例子 . 例如,较快的版本在构造函数中使用 foreach ,它不会导致性能问题 .

    最后,这都是透视问题 . 在对象上调用方法比直接调用函数和过程要慢,这是面向对象编程的一个主要反对意见,但事实证明它在大多数情况下并不是一个大问题 .

  • 6

    有一点需要注意:Java 7将为JVM引入一个新的invokedynamic字节码,这将使很多Groovy的"metaclass magic"不必要的,应该大大加快JVM上的动态语言实现 .

  • 6

    您可以将Java音译为Scala,最终得到几乎完全相同的字节码 . 因此Scala完全能够像Java一样快 .

    也就是说,有很多方法可以编写速度更慢,内存更密集的Scala代码,这些代码比Java等效代码更短,更易读 . 这很好!我们使用Java而不是C,因为内存保护可以改进我们的代码 . Scala的额外表现力意味着您可以编写更短的程序,而不是像Java那样更少的程序 . 有时会伤害性能,但大部分时间都没有 .

  • 10

    反义词和David已经涵盖了关于Scala的要点:它基本上和Java一样快,它就是这样,因为它是静态类型的(因此不需要额外的运行时检查)并使用JVM通常可以完全删除的轻量级包装器 .

    Scala确实很容易使用强大的通用库功能 . 与Java中任何强大的通用库一样,它具有一些与之相关的性能损失 . 例如,使用java.util.HashMap在字节和字节之间实现映射在Java中会非常缓慢(与原始数组查找表相比),并且在Scala中同样会很慢 . 但Scala为您提供了更多此类功能,并且可以非常轻松地调用它们,以至于您可以在非常少的代码中完成大量工作 . 和往常一样,当你很容易提出要求时,人们有时会要求很多,然后想知道为什么需要这么长时间 . (当人们在幕后学习(或仔细思考)必须发生的事情时,易于询问会让人更加惊讶 . )

    可能引起的唯一合理批评是Scala并没有像编写高性能代码那样容易 . 大多数易于使用的功能都针对通用函数编程,它仍然非常快,但不如直接访问原始类型那么快 . 例如,Scala有一个非常强大的 for 循环,但它使用泛型类型,因此原语必须加框,因此你不能有效地使用它来迭代原始数组;你必须使用 while 循环 . (性能差异很可能会在2.8中减少,其中提到的是反义词 . )

  • 1

    其他答案集中在scala的细节上 . 我想为通用案例添加一些要点 . 首先,编写一个字节码生成器是非常可行的,它生成类似javac的代码,但是来自非java的语言 . 由于语言语义与java的语义不同,这变得更加困难 . 然而,显式类型不是语义的一部分,只是语法的一部分(并且它具有错误检测属性) .

    在不能静态地(在编译时)确定类型的情况下,或者如果语言本质上是动态的(类似于javascript,jython,jruby等许多脚本语言中的类型是动态的),性能会降低 . 在使用1.6 jdk的情况下,您需要进行一些基于反射的调度 . 这显然较慢,并且无法通过热点/虚拟机轻松优化 . Jdk 1.7扩展了invokedynamic,以便它可以实际用于以脚本语言支持的动态方式调用函数 .

    javac编译器没有做那么多的优化(jvm在运行时完成它们)所以java语言很简单地映射到java字节码 . 这意味着与具有不同语义的语言相比,具有相同语义的语言具有优势 . 这是JVM的缺点,也是CLR(.net运行时)和LLVM具有明显优势的地方 .

相关问题