batchのブログ

知見の備忘録

Kotlinのinlineとcrossinlineの挙動をバイトコードで追う

この記事は、Kotlinのinlineとcrossinlineの挙動をバイトコードで追う - Qiita のコピーです。

ただのメソッドの呼び出し

Kotlinでこのようなコードを書いたとき

fun main() {
    doSomeThing()
}

fun doSomeThing() {
    println("This is doSomeThing")
}

バイトコードはこんな感じになってます。 main()の中で doSomeThing()を呼び出すということを行っています。

public final class InlineCrossinlineKt {
   public static final void main() {
      doSomeThing();
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void doSomeThing() {
      String var0 = "This is doSomeThing";
      boolean var1 = false;
      System.out.println(var0);
   }
}

inlineがやること

さっきまであった main()の中の doSomeThing()の呼び出しがなくなって、 doSomeThing()の中身が main()の中に展開された形になります。これがinlineの性質です。他のメソッドを呼び出すというのは少なからずコストがかかることで、それをなくす形で同じように実行してくれるというものです。

fun main() {
    doSomeThing()
}

inline fun doSomeThing() {
    println("This is doSomeThing")
}

バイトコード

public final class InlineCrossinlineKt {
   public static final void main() {
      int $i$f$doSomeThing = false;
      String var1 = "This is doSomeThing";
      boolean var2 = false;
      System.out.println(var1);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void doSomeThing() {
      int $i$f$doSomeThing = 0;
      String var1 = "This is doSomeThing";
      boolean var2 = false;
      System.out.println(var1);
   }
}

ちなみにこのコードをIDEで書いた場合、このinlineはあまり意味がないよとワーニングが起こり、inlineをつけるときは、引数に関数型を持つメソッドにつけるとbestだよと言われます。

Screen_Shot_2021-03-25_at_1.25.03.png

bestなinline

doSomeThing()にラムダを引数に取るように変更しました。さきほどのワーニングも消えました。

fun main() {
    doSomeThing {
        println("This is block")
    }
}

inline fun doSomeThing(block: () -> Unit) {
    println("This is doSomeThing")
    block()
}

このときのバイトコードがどうなってるか同じように確認してみましょう。

public final class InlineCrossinlineKt {
   public static final void main() {
      int $i$f$doSomeThing = false;
      String var1 = "This is doSomeThing";
      boolean var2 = false;
      System.out.println(var1);
      int var3 = false;
      String var4 = "This is block";
      boolean var5 = false;
      System.out.println(var4);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void doSomeThing(@NotNull Function0 block) {
      int $i$f$doSomeThing = 0;
      Intrinsics.checkNotNullParameter(block, "block");
      String var2 = "This is doSomeThing";
      boolean var3 = false;
      System.out.println(var2);
      block.invoke();
   }
}

先ほどと同じように、 doSomeThing()の中の This is doSomeThingはinlineの影響で main()の中で直接 println()されたかのように振る舞います。

そして、今回 doSomeThing()の引数に追加したラムダである block()doSomeThing() で実行されて This is block が出力されるわけではなく、 main()の中で block()の処理が直接展開されて実行されます。

実行結果は一緒ですが、中ではこのような処理の違いが起こります。

そして、ラムダの前に crossinlineをつけてdoSomeThing(crossinline block: () → Unit)としたとき、バイトコードは同じになります。

なので、 inline funの引数がラムダを持つとき、そのラムダは crossinlineの影響によって、そのラムダの中身の処理は呼び出し元の main()の中に展開されたような振る舞いをします。

ちなみに、 block()noinlineをつけた場合のバイトコードを見てみましょう。

fun main() {
    doSomeThing {
        println("This is block")
    }
}

inline fun doSomeThing(noinline block: () -> Unit) {
    println("This is doSomeThing")
    block()
}

この場合、 inlineの影響で doSomeThing()の処理である This is doSomeThingの出力は main()の中で行われますが、 block()doSomeThing()の中で block.invoke()が呼ばれて実行されています。

public final class InlineCrossinlineKt {
   public static final void main() {
      Function0 block$iv = (Function0)null.INSTANCE;
      int $i$f$doSomeThing = false;
      String var2 = "This is doSomeThing";
      boolean var3 = false;
      System.out.println(var2);
      block$iv.invoke();
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void doSomeThing(@NotNull Function0 block) {
      int $i$f$doSomeThing = 0;
      Intrinsics.checkNotNullParameter(block, "block");
      String var2 = "This is doSomeThing";
      boolean var3 = false;
      System.out.println(var2);
      block.invoke();
   }
}

inlineのメリット・デメリット

inline をつけたメソッドを定義することで、そのメソッドの中身の処理がそのままコピーされたように呼び出し元に展開されて実行される挙動になります。これにより、inline メソッドの中身の処理が呼び出し元にそのまま書かれているように実行され、メソッドを呼ぶコストを削減できます。 inline メソッドの中身の処理によると思いますが、少しパフォーマンスの良いコードを書くことができるようになるというわけです。

じゃあ、全部 inline つければすっごいパフォーマンスいいコード書けるようになるんじゃないか?と思うかもしれません。

しかし、Decompileしたバイトコードを見てみると、 inline をつけた場合その中身の処理のコピーが呼び出し元に展開されて、 inline メソッドを呼び出せば呼び出すほどこのコピーが増えてバイトコードの容量も大きくなっていきます。なので、 inline をつければつけるほど apkファイルなどのファイルサイズが大きくなってしまうことが挙げられます。これが inline をつけるときのデメリットというか、適材適所という訳です。

なので、 inline メソッドは中身の処理はそこまで多くないけど、色々なとこで呼ばれるケースがあるとき効果を発揮します。