【问题标题】:Groovy / Scala / Java under the hood底层的 Groovy / Scala / Java
【发布时间】:2010-03-27 15:02:18
【问题描述】:

我使用 Java 大约有 6 到 7 年的时间,然后几个月前我发现了 Groovy 并开始节省大量的打字时间。然后我想知道某些事情是如何实现的在幕后工作(因为 groovy 的性能真的很差),并且明白为了给你动态类型,每个 Groovy 对象都是一个 MetaClass 对象,它处理所有的事情JVM 无法自行处理。当然,这会在您编写的内容和执行的内容之间引入一个中间层,这会减慢一切。

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

我的意思是,Scala静态类型的,所以 Java 类的包装器应该更轻,因为在编译时会检查很多东西,但我我不确定里面发生了什么的真正差异。 (我不是在谈论 Scala 与其他功能相比的功能方面,这是另一回事)

谁能启发我?

从 WizardOfOdds 的评论看来,获得更少打字和相同性能的唯一方法是编写一个中间翻译器,在 Java 代码中翻译某些东西(让 javac 编译它)而不改变事情的方式被执行,只是添加同步糖而不关心语言本身的其他后备。

【问题讨论】:

  • @Jack:Scala 比 Groovy 快得多,但与 Java 相比,它仍然很慢。使用 Scala 编程比使用 Java 编程更容易自爆(确切地知道何时将使用原始数组或不使用 Scala 中的“幕后”可以杀死性能您的应用程序)。如果您想阅读有趣的(?),那么您应该在 Twitter 上阅读有关 Scala/Java 使用的信息。基本上,在他们需要良好性能的每一个地方,他们都不得不退回到 Java。 Java 变得和 C 一样快,在某些情况下,由于 JIT,它比 C 更快。有点难以击败;)
  • “狗比 Java 慢” ??我不同意。在 Scala 2.8 中,数组是 JVM 原生数组,而不是盒装数组。对于代码中的热点,您始终可以编写不使用函数文字/闭包的循环。基本上,在大多数情况下,少量的知识和生成高效代码的常用良好实践就足以将 Scala 和 Java 之间的差距缩小到可以忽略不计的程度。
  • @Wizard:看看 shootout.alioth.debian.org 上的基准。然后,您是否想冒险猜测一下 Twitter 的人是否对 Scala 不太熟悉,而不是因为它是该语言的一些内在限制?
  • @WizardOfOdds 你有关于推特的参考吗?我以前从未听说过,而且 Scala 社区中有一些 twitter 人。
  • @WizardOfOdds:如果你知道你在说什么,因为你的 long[] 是数百 MB,那么我必须知道我在说什么,因为我的 Array[Float] 是数百 MB 并且提供 Java 级别的性能。不过,说真的,如果你想对高性能 Scala 采取简单的方法并且你知道高性能 Java,那么使用 Scala 的子集,就像 Java 一样工作(原始类型、Array[Primitive] 和 while 循环),你会一样快。 Scala 中的基元自动装箱很少比 Java 中更令人困惑,只是你有动力在 Scala 中更频繁地这样做。

标签: java scala groovy bytecode


【解决方案1】:

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

在代码中的 cmets inline 中,我解释了数组访问、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

结构类型(又名 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 被转换为对高阶函数的调用链: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

行号表: 第 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 对象没有 no 包装。例如,以下两个类分别在 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)
}

这也是一个很好的例子,说明如何在需要时完全平衡性能。例如,更快的版本在构造函数中使用foreach,不会导致性能问题。

归根结底,这完全是一个观点问题。在对象上调用方法比直接调用函数和过程要慢,这是对面向对象编程的主要反对意见,但大多数时候它并不是什么大问题。

【讨论】:

    【解决方案3】:

    需要注意的一点:Java 7 将为 JVM 引入一个新的 invokedynamic 字节码,这将使 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.6 jdk 的情况下,您需要进行一些基于反射的调度。这显然速度较慢,无法通过热点/虚拟机轻松优化。 Jdk 1.7 扩展了invokedynamic,因此它实际上可以用于以脚本语言支持的动态方式调用函数。

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2021-04-22
            • 2010-12-18
            • 2021-06-02
            • 2012-10-30
            • 2012-01-13
            • 2011-03-20
            • 2023-04-01
            相关资源
            最近更新 更多