簡體   English   中英

引擎蓋下的Groovy / Scala / Java

[英]Groovy / Scala / Java under the hood

我用了6到7年的Java ,然后幾個月前我發現了Groovy並且開始節省很多打字..然后我想知道某些事情是如何在幕后工作的(因為時髦的表現真的很差)並且明白要給予動態輸入每個Groovy對象都是一個MetaClass對象,它處理JVM無法自行處理的所有事情。 當然,這會在您編寫的內容和執行的內容之間引入一層,從而減慢所有內容。

然后有一天我開始得到一些關於Scala的信息。 這兩種語言在字節碼翻譯中的比較如何? 他們添加到普通Java代碼可以獲得的正常結構中有多少東西?

我的意思是, Scala靜態類型的,因此Java類的包裝應該更輕,因為在編譯期間會檢查很多東西,但我不確定內部存在的真正差異。 (我不是在談論Scala的功能方面與其他方面相比,這是另一回事)

有人可以開導我嗎?

從WizardOfOdds注釋看來,獲得更少打字和相同性能的唯一方法是編寫一個中間翻譯器,用Java代碼翻譯某些東西(讓javac編譯它)而不改變事情的執行方式,只需添加同義詞糖即可。語言本身的其他后備。

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

很多好的答案,我會嘗試添加我從你的問題中得到的其他東西。 沒有包裝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子類中),這可能導致性能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 ,例如,它不會導致性能問題。

最后,這都是透視問題。 在對象上調用方法比直接調用函數和過程要慢,這是面向對象編程的一個主要反對意見,但事實證明它在大多數情況下並不是一個大問題。

需要注意的一件事是:Java 7將為JVM引入一個新的invokedynamic字節碼,這將使Groovy的“元類魔術”變得不必要,並且應該大大加快JVM上的動態語言實現。

您可以將Java音譯為Scala,最終得到幾乎完全相同的字節碼。 因此Scala完全能夠像Java一樣快。

也就是說,有很多方法可以編寫速度更慢,內存更密集的Scala代碼,這些代碼比Java等效代碼更短,更易讀。 這很好! 我們使用Java而不是C,因為內存保護可以改進我們的代碼。 Scala的額外表現力意味着您可以編寫更短的程序,而不是像Java那樣更少的程序。 有時會傷害性能,但大部分時間都沒有。

反義詞和David已經涵蓋了關於Scala的要點:它基本上和Java一樣快,它就是這樣,因為它是靜態類型的(因此不需要額外的運行時檢查)並使用JVM通常可以完全刪除的輕量級包裝器。

Scala確實很容易使用強大的通用庫功能。 與Java中任何強大的通用庫一樣,它具有一些與之相關的性能損失。 例如,使用java.util.HashMap在字節和字節之間實現映射在Java中會非常緩慢(與原始數組查找表相比),並且在Scala中同樣會很慢。 但Scala為您提供了更多此類功能,並且可以非常輕松地調用它們,以至於您可以在非常少的代碼中完成大量工作。 和往常一樣,當你很容易提出要求時,人們有時會要求很多,然后想知道為什么需要這么長時間。 (當人們在幕后學習(或仔細思考)必須發生的事情時,易於詢問會讓人更加驚訝。)

可能引起的唯一合理批評是Scala並沒有像編寫高性能代碼那樣容易。 大多數易於使用的功能都針對通用函數編程,它仍然非常快,但不如直接訪問原始類型那么快。 例如,Scala具有令人難以置信的強大for循環,但它使用泛型類型,因此原語必須加框,因此您無法有效地使用它來迭代原始數組; 你必須使用while循環。 (性能差異很可能會在2.8中減少,其中提到的是反義詞。)

其他答案集中在scala的細節上。 我想為通用案例添加一些要點。 首先,編寫一個字節碼生成器是非常可行的,它生成類似javac的代碼,但是來自非java的語言。 由於語言語義與java的語義不同,這變得更加困難。 然而,顯式類型不是語義的一部分,只是語法的一部分(並且它具有錯誤檢測屬性)。

在不能靜態地(在編譯時)確定類型的情況下,或者如果語言本質上是動態的(鍵入是動態的,如javascript,jython,jruby等許多腳本語言中),性能會降低。 在使用1.6 jdk的情況下,您需要進行一些基於反射的調度。 這顯然較慢,並且無法通過熱點/虛擬機輕松優化。 Jdk 1.7擴展了invokedynamic,以便它可以實際用於以腳本語言支持的動態方式調用函數。

javac編譯器沒有做那么多的優化(jvm在運行時完成它們)所以java語言很簡單地映射到java字節碼。 這意味着與具有不同語義的語言相比,具有相同語義的語言具有優勢。 這是JVM的缺點,也是CLR(.net運行時)和LLVM具有明顯優勢的地方。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM