[英]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
被轉換為對更高階函數的調用鏈: foreach
, map
, flatMap
和withFilter
。 這非常強大,但您需要注意以下代碼與Java中的類似外觀構造的效率差不多。 Scala 2.8將@specialize Function1至少為Double
和Int
,並希望@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.