Kotlin、バイトコードのコンパイルとパフォーマンス(パート2)





これは、出版物の続きです。 最初の部分はここで見ることができます



内容:



サイクル

いつ

代表者

オブジェクトとコンパニオンオブジェクト

lateinitプロパティ

コルーチン

結論



ループ:



Kotlinには、Javaのような古典的な3つの部分がありません。 一部にはこれは問題のように思えるかもしれませんが、そのようなサイクルを使用するすべてのケースをより詳細に見ると、ほとんどの場合、値をソートするために使用されていることがわかります。 Kotlinは、それを置き換えるための簡素化された設計をしています。



//Kotlin fun rangeLoop() { for (i in 1..10) { println(i) } }
      
      





1..10は、反復が発生する範囲です。 Kotlinコンパイラーは十分に賢く、この場合に何をしようとしているかを理解しているため、不要なオーバーヘッドをすべて除去します。 コードは、ループカウンター変数を使用して通常のwhileループにコンパイルされます。 イテレータもオーバーヘッドもありません。すべてが十分にコンパクトです。



 //Java public static final void rangeLoop() { int i = 1; byte var1 = 10; if(i <= var1) { while(true) { System.out.println(i); if(i == var1) { break; } ++i; } } }
      
      





配列(KotlinではArray <*>として記述されている)を介した同様のループは、forループに同様にコンパイルされます。



 //Kotlin fun arrayLoop(x: Array<String>) { for (s in x) { println(s) } }
      
      





 //Java public static final void arrayLoop(@NotNull String[] x) { Intrinsics.checkParameterIsNotNull(x, "x"); for(int var2 = 0; var2 < x.length; ++var2) { String s = x[var2]; System.out.println(s); } }
      
      





リストのアイテムを反復処理する場合、わずかに異なる状況が発生します。



 //Kotlin fun listLoop(x: List<String>) { for (s in x) { println(s) } }
      
      





この場合、イテレーターを使用する必要があります。



 //Java public static final void listLoop(@NotNull List x) { Intrinsics.checkParameterIsNotNull(x, "x"); Iterator var2 = x.iterator(); while(var2.hasNext()) { String s = (String)var2.next(); System.out.println(s); } }
      
      





したがって、反復する要素に応じて、Kotlinコンパイラーはループをバイトコードに変換する最も効率的な方法を選択します。



以下は、Javaで同様のソリューションを使用したループのパフォーマンス比較です。



サイクル







ご覧のとおり、KotlinとJavaの違いはごくわずかです。 バイトコードはjavacが生成するものに非常に近いです。 開発者によると、結果のバイトコードがjavacが生成するパターンにできるだけ近くなるように、今後のKotlinのバージョンでこれを改善する予定です。



いつ



WhenはJavaからのスイッチに類似していますが、より多くの機能を備えています。 以下のいくつかの例を見てみましょう。



 /Kotlin fun tableWhen(x: Int): String = when(x) { 0 -> "zero" 1 -> "one" else -> "many" }
      
      





このような単純な場合、結果のコードは通常のスイッチにコンパイルされますが、ここでは魔法は発生しません。



 //Java public static final String tableWhen(int x) { String var10000; switch(x) { case 0: var10000 = "zero"; break; case 1: var10000 = "one"; break; default: var10000 = "many"; } return var10000; }
      
      





上記の例を少し変更し、定数を追加する場合:



 //Kotlin val ZERO = 1 val ONE = 1 fun constWhen(x: Int): String = when(x) { ZERO -> "zero" ONE -> "one" else -> "many" }
      
      





この場合のコードは、すでに次の形式にコンパイルされています。



 //Java public static final String constWhen(int x) { return x == ZERO?"zero":(x == ONE?"one":"many"); }
      
      





これは、現時点では、Kotlinコンパイラーが値が定数であることを理解しておらず、スイッチに変換する代わりに、コードが一連の比較に変換されるためです。 したがって、一定の時間の代わりに、線形への移行があります(比較の回数に応じて)。 この言語の開発者によると、これは将来簡単に修正できますが、現在のバージョンではまだそうなっています。



コンパイル時に既知の定数にconst修飾子を使用することもできます。

 //Kotlin ( When2.kt) const val ZERO = 1 const val ONE = 1 fun constWhen(x: Int): String = when(x) { ZERO -> "zero" ONE -> "one" else -> "many" }
      
      





次に、この場合、コンパイラはすでに正しく最適化されています:

 public final class When2Kt { public static final int ZERO = 1; public static final int ONE = 2; @NotNull public static final String constWhen(int x) { String var10000; switch(x) { case 1: var10000 = "zero"; break; case 2: var10000 = "one"; break; default: var10000 = "many"; } return var10000; } }
      
      





定数をEnumで置き換える場合:



 //Kotlin ( When3.kt) enum class NumberValue { ZERO, ONE, MANY } fun enumWhen(x: NumberValue): String = when(x) { NumberValue.ZERO -> "zero" NumberValue.ONE -> "one" NumberValue.MANY -> "many" }
      
      





そのコードは、最初の場合と同様に、スイッチにコンパイルされます(実際のコードは、Javaの列挙列挙の場合と同じです)。



 //Java public final class When3Kt$WhenMappings { // $FF: synthetic field public static final int[] $EnumSwitchMapping$0 = new int[NumberValue.values().length]; static { $EnumSwitchMapping$0[NumberValue.ZERO.ordinal()] = 1; $EnumSwitchMapping$0[NumberValue.ONE.ordinal()] = 2; $EnumSwitchMapping$0[NumberValue.MANY.ordinal()] = 3; } } public static final String enumWhen(@NotNull NumberValue x) { Intrinsics.checkParameterIsNotNull(x, "x"); String var10000; switch(When3Kt$WhenMappings.$EnumSwitchMapping$0[x.ordinal()]) { case 1: var10000 = "zero"; break; case 2: var10000 = "one"; break; case 3: var10000 = "many"; break; default: throw new NoWhenBranchMatchedException(); } return var10000; }
      
      





要素の序数によってスイッチ内のブランチの数が決まり、それによって目的のブランチが選択されます。



KotlinとJavaのソリューションのパフォーマンスの比較を見てみましょう。



いつ







どうやら単純なスイッチはまったく同じように動作します。 Kotlinコンパイラーが変数が定数であると判断できず、比較に移った場合、Javaは少し速く実行されます。 また、enum値をソートする状況では、ordinalの値によるブランチの定義の混乱もわずかに失われます。 しかし、これらの欠点はすべて将来のバージョンで修正され、さらにパフォーマンスの低下はそれほど大きくなく、重要な場所ではコードを別のオプションに書き換えることができます。 使いやすさのためにかなりリーズナブルな価格。



代表者



委任は継承の優れた代替手段であり、Kotlinはそのまま継承をサポートします。 単純なクラス委任の例を考えてみましょう。



 //Kotlin package examples interface Base { fun print() } class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } class Derived(b: Base) : Base by b { fun anotherMethod(): Unit {} }
      
      





コンストラクターのDerivedクラスは、Baseインターフェイスを実装するクラスのインスタンスを受け取り、次に、Baseインターフェイスのすべてのメソッドの実装を、渡されたインスタンスに委任します。 逆コンパイルされた派生クラスのコードは次のようになります。



 public final class Derived implements Base { private final Base $$delegate_0; public Derived(@NotNull Base b) { Intrinsics.checkParameterIsNotNull(b, "b"); super(); this.$$delegate_0 = b; } public void print() { this.$$delegate_0.print(); } public final void anotherMethod() { } }
      
      





クラスコンストラクターにはクラスのインスタンスが渡され、このインスタンスは不変の内部フィールドに格納されます。 Baseインターフェイスのprintメソッドもオーバーライドされます。このメソッドはデリゲートから単に呼び出されます。 すべてが非常に簡単です。



クラス全体の実装だけでなく、個々のプロパティも委任することもできます(バージョン1.1以降では、ローカル変数で初期化を委任することも可能です)。



コトリンコード:



 //Kotlin class DeleteExample { val name: String by Delegate() }
      
      





コードにコンパイル:



 public final class DeleteExample { @NotNull private final Delegate name$delegate = new Delegate(); static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(DeleteExample.class), "name", "getName()Ljava/lang/String;"))}; @NotNull public final String getName() { return this.name$delegate.getValue(this, $$delegatedProperties[0]); } }
      
      





DeleteExampleクラスが初期化されると、Delegateクラスのインスタンスが作成され、name $ delegateフィールドに保存されます。 そして、getName関数の呼び出しは、name $ delegateからgetValue関数の呼び出しにリダイレクトされます。



Kotlinにはすでにいくつかの標準デリゲートがあります:



-遅延、フィールド値の遅延計算用。

-observable。フィールド値のすべての変更の通知を受信できます。

-マップ値からフィールド値を初期化するために使用されるマップ。



オブジェクトとコンパニオンオブジェクト



Kotlinには、メソッドとフィールドの静的修飾子がありません。 代わりに、ほとんどの場合、ファイルレベルの関数を使用することをお勧めします。 クラスのインスタンスなしで呼び出すことができる関数を宣言する必要がある場合、このためにオブジェクトとコンパニオンオブジェクトがあります。 バイトコードでどのように見えるかの例を見てみましょう。



1つのメソッドを持つ単純なオブジェクト宣言は次のとおりです。



 //Kotlin object ObjectExample { fun objectFun(): Int { return 1 } }
      
      





コードでは、ObjectExampleのインスタンスを作成せずにobjectFunメソッドを呼び出すことができます。 コードはほとんど標準的なシングルトンにコンパイルされます。



 public final class ObjectExample { public static final ObjectExample INSTANCE; public final int objectFun() { return 1; } private ObjectExample() { INSTANCE = (ObjectExample)this; } static { new ObjectExample(); } }
      
      





そして、呼び出しの場所:



 //Kotlin val value = ObjectExample.objectFun()
      
      





INSTANCE呼び出しにコンパイル:



 //Java int value = ObjectExample.INSTANCE.objectFun();
      
      





コンパニオンオブジェクトは、インスタンスを作成することになっているクラスでのみ同様のメソッドを作成するために使用されます。



 //Kotlin class ClassWithCompanion { val name: String = "Kurt" companion object { fun companionFun(): Int = 5 } } //method call ClassWithCompanion.companionFun()
      
      





companionFunメソッドを呼び出すことも、クラスのインスタンスを必要としません。Kotlinでは、静的メソッドの単純な呼び出しのように見えます。 しかし実際、クラスの仲間には魅力があります。 逆コンパイルされたコードを見てみましょう:



 //Java public final class ClassWithCompanion { @NotNull private final String name = "Kurt"; public static final ClassWithCompanion.Companion Companion = new ClassWithCompanion.Companion((DefaultConstructorMarker)null); @NotNull public final String getName() { return this.name; } public static final class Companion { public final int companionFun() { return 5; } private Companion() { } public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } } //  ClassWithCompanion.Companion.companionFun();
      
      





Kotlinコンパイラーは呼び出しを単純化しますが、Javaからはそれほどきれいに見えなくなります。 幸いなことに、メソッドを本当に静的に宣言することができます。 これには@JvmStatic注釈があります。 オブジェクトメソッドとコンパニオンオブジェクトメソッドの両方に追加できます。 オブジェクトの例を考えてみましょう。



 //Kotlin object ObjectWithStatic { @JvmStatic fun staticFun(): Int { return 5 } }
      
      





この場合、staticFunメソッドは実際に静的と宣言されます。



 public final class ObjectWithStatic { public static final ObjectWithStatic INSTANCE; @JvmStatic public static final int staticFun() { return 5; } private ObjectWithStatic() { INSTANCE = (ObjectWithStatic)this; } static { new ObjectWithStatic(); } }
      
      





コンパニオンオブジェクトのメソッドの場合、@ JvmStaticアノテーションを追加することもできます。



 class ClassWithCompanionStatic { val name: String = "Kurt" companion object { @JvmStatic fun companionFun(): Int = 5 } }
      
      





静的なcompanionFunメソッドもそのようなコード用に作成されます。 ただし、メソッド自体は引き続きコンパニオンからメソッドを呼び出します。



 public final class ClassWithCompanionStatic { @NotNull private final String name = "Kurt"; public static final ClassWithCompanionStatic.Companion Companion = new ClassWithCompanionStatic.Companion((DefaultConstructorMarker)null); @NotNull public final String getName() { return this.name; } @JvmStatic public static final int companionFun() { return Companion.companionFun(); } public static final class Companion { @JvmStatic public final int companionFun() { return 5; } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } }
      
      





上記のように、Kotlinは静的メソッドとコンパニオンメソッドの両方を宣言するためのさまざまなオプションを提供します。 静的メソッドの呼び出しは少し速くなります。したがって、パフォーマンスが重要な場所では、メソッドに@JvmStatic注釈を付けることをお勧めします(ただし、速度の大幅な向上に頼るべきではありません)



lateinitプロパティ



クラスでnotnullプロパティを宣言する必要があるときに、すぐに値を指定できない場合があります。 ただし、notnullフィールドを初期化するときは、デフォルト値を割り当てるか、Nullableプロパティを作成してnullを書き込む必要があります。 nullにならないようにするために、Kotlinには、後でプロパティを初期化することをコミットすることをKotlinコンパイラーに伝える特別なlateinit修飾子があります。



 //Kotlin class LateinitExample { lateinit var lateinitValue: String }
      
      





初期化せずにプロパティにアクセスしようとすると、UninitializedPropertyAccessExceptionがスローされます。 このような機能は非常に簡単に機能します。



 //Java public final class LateinitExample { @NotNull public String lateinitValue; @NotNull public final String getLateinitValue() { String var10000 = this.lateinitValue; if(this.lateinitValue == null) { Intrinsics.throwUninitializedPropertyAccessException("lateinitValue"); } return var10000; } public final void setLateinitValue(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.lateinitValue = var1; } }
      
      





プロパティ値の追加チェックがゲッターに挿入され、nullが格納されている場合、例外がスローされます。 ちなみに、まさにこのため、Kotlinでは、Int、Long、およびJavaのプリミティブ型に対応する他の型を持つlateinitプロパティを作成することはできません。



コルーチン



Kotlin 1.1には、コルーチンと呼ばれる新しい機能が導入されています。 その助けを借りて、非同期コードを同期形式で簡単に書くことができます。 割り込みサポート用のメインライブラリ(kotlinx-coroutines-core)に加えて、さまざまな拡張機能を備えた多数のライブラリセットもあります。



kotlinx-coroutines-jdk8-JDK8の追加ライブラリ

kotlinx-coroutines-nio-JDK7 +からの非同期IOの拡張。



kotlinx-coroutines-reactive-リアクティブストリームユーティリティ

kotlinx-coroutines-reactor-Reactorのユーティリティ

kotlinx-coroutines-rx1-RxJava 1.xのユーティリティ

kotlinx-coroutines-rx2-RxJava 2.xのユーティリティ



kotlinx-coroutines-android-AndroidのUIコンテキスト。

kotlinx-coroutines-javafx-JavaFX UIアプリケーションのJavaFxコンテキスト。

kotlinx-coroutines-swing-Swing UIアプリケーションのSwingコンテキスト。



注:機能はまだ実験段階であるため、以下に記載されているすべてが変更される可能性があります。



サスペンド修飾子は、関数が割り込み可能であり、割り込みのコンテキストで使用できることを示すために使用されます。



 //Kotlin suspend fun asyncFun(x: Int): Int { return x * 3 }
      
      





逆コンパイルされたコードは次のとおりです。



 //Java public static final Object asyncFun(int x, @NotNull Continuation $continuation) { Intrinsics.checkParameterIsNotNull($continuation, "$continuation"); return Integer.valueOf(x * 3); }
      
      





Continuationインターフェースを実装する1つの追加パラメーターがまだ渡されることを除いて、ほとんど元の関数が判明しています。



 interface Continuation<in T> { val context: CoroutineContext fun resume(value: T) fun resumeWithException(exception: Throwable) }
      
      





実行コンテキストを保存し、結果を返す関数とエラーの場合に例外を返す関数を定義します。



コルーチンは、ステートマシンにコンパイルされます。 例を考えてみましょう:



 val a = a() val y = foo(a).await() //   #1 b() val z = bar(a, y).await() //   #2 c(z)
      
      





fooおよびbar関数は、await suspend関数が呼び出されるCompletableFutureを返します。 このようなコードはJavaで逆コンパイルできません(ほとんどの場合gotoによる)ので、擬似コードで検討してください。



 class <anonymous_for_state_machine> extends CoroutineImpl<...> implements Continuation<Object> { //     int label = 0 //    A a = null Y y = null void resume(Object data) { if (label == 0) goto L0 if (label == 1) goto L1 if (label == 2) goto L2 else throw IllegalStateException() L0: a = a() label = 1 data = foo(a).await(this) // 'this'   continuation if (data == COROUTINE_SUSPENDED) return // ,  await   L1: //     ,    data y = (Y) data b() label = 2 data = bar(a, y).await(this) // 'this'   continuation if (data == COROUTINE_SUSPENDED) return // ,  await   L2: //         data Z z = (Z) data c(z) label = -1 //      return } }
      
      





ご覧のとおり、L0、L1、L2の3つの状態が取得されます。 実行は状態L0で開始され、そこから状態L1への切り替えが行われ、次にL2への切り替えが行われます。 最後に、状態は-1に切り替わり、これ以上のステップが許可されないことを示します。



コルーチン自体はさまざまなスレッドで実行できます。コルーチンを起動するコンテキストでプールを指定することにより、これを制御する便利なメカニズムがあります。 多くの例とその使用法の説明を含む詳細なガイドを見ることができます。



すべてのKotlinソースコードはgithubで入手できます。 それらを自宅で開き、コードを試して、ソースがコンパイルされる最終バイトコードを同時に確認できます。



結論



KotlinでのアプリケーションのパフォーマンスはJavaでのパフォーマンスよりもはるかに悪くなることはありません。また、インライン修飾子を使用した方が優れている場合もあります。 すべての場所のコンパイラは、最も最適化されたバイトコードを生成しようとします。 したがって、Kotlinに切り替えると、パフォーマンスが大幅に低下することを恐れないでください。 また、特に重要な場所では、Kotlinのコンパイル先がわかっているため、いつでもコードをより適切なオプションに書き換えることができます。 この言語を使用すると、かなり簡潔でシンプルな形式で複雑な設計を実装できるという事実に対して少額の費用がかかります。



ご清聴ありがとうございました! 記事をお楽しみください。 エラーや不正確さに気付いた人はすべて、このことについて個人的なメッセージで私に書いてください。



All Articles