LoginSignup
76
41

More than 1 year has passed since last update.

[Kotlin]インライン関数を理解する(inline, noinline, crossinline)

Last updated at Posted at 2019-10-09

はじめに

Kotlinのインライン関数については公式リファレンスに説明があるのですが、
inlineやnoinline、crossinlineについては少し分かりづらい点がありました。
そこで実際にコードを書いてみてinlineやnoinline、crossinlineで何ができるか調べて行きたいと思います。

インライン化とは何か?

インライン化とはインライン展開のことで、Wikipediaでは次のように説明されています。

インライン展開(インラインてんかい、英: inline expansion または 英: inlining)とは、コンパイラによる最適化手法の1つで、
関数を呼び出す側に呼び出される関数のコードを展開し、関数への制御転送をしないようにする手法。これにより関数呼び出しに伴うオーバーヘッドを削減する。

関数呼び出しに伴うオーバーヘッドを軽減するとありますが、
どのように変化するのかインライン化した場合としない場合で
コードを書いてみて生成されるバイトコードをチェックしてみます。

インライン化しない場合

Kotlin

fun main(args: Array<String>): Unit {
    hello()
}

private fun hello() {
    print("Hello")
}

Java (バイトコードをデコンパイル)

public final class HelloKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");

      // -------------------------------------------
      // インライン化されておらず関数がそのまま呼ばれている
      // -------------------------------------------
      hello();
      // -------------------------------------------
   }

   private static final void hello() {
      String var0 = "Hello";
      System.out.print(var0);
   }
}

インライン化した場合

Kotlin

package inline

fun main(args: Array<String>): Unit {
    hello()
}

private inline fun hello() {
    print("Hello")
}

Java(バイトコードをデコンパイル)

public final class InlineKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      // -------------------------------------------
    // 関数呼び出さずに関数の内容をそのまま実行している
      // -------------------------------------------
      String var1 = "Hello";
      System.out.print(var1);
      // -------------------------------------------
   }

   private static final void hello() {
      String var1 = "Hello";
      System.out.print(var1);
   }
}

このようにインライン化した場合には関数を呼び出しの操作が1段少なくなります。
関数呼び出しの回数を減らし、それに伴うオーバーヘッドも軽減できるため高速化に繋がるというわけです。

💡 オーバーヘッドとはスタックフレーム(引数,リターンアドレス情報)をコールスタックに積む操作のことです

inline修飾子で関数をインライン化

インライン化の説明のときに既に出てきていますが次のように
関数にinline修飾子をつけるとコンパイラがその関数をインライン化してくれます。
このようにinline修飾子をつけた関数をインライン関数と呼びます。

Kotlin

fun main(args: Array<String>): Unit {
    val sum = plus(10, 10)
    println(sum)
}

private inline fun plus(a: Int, b: Int) : Int{
    return a + b
}

Java(バイトコードをデコンパイル)

public final class InlineFuncKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      // -------------------------------------------
      // plus関数がインライン化されている
      // -------------------------------------------      
      byte a$iv = 10;
      int b$iv = 10;
      int sum = a$iv + b$iv;
      // -------------------------------------------
      System.out.println(sum);
   }

   private static final int plus(int a, int b) {
      return a + b;
   }
}

インライン関数では引数のラムダもインライン化

Kotlinのインライン関数は関数本体だけでなく、
引数に指定したラムダも含めてインライン化してくれます。

Kotlin

fun main(args: Array<String>): Unit {
    val obj: Lock = ReentrantLock()
    lock(obj) {
        println("Hello")
    }
}

private inline fun lock(l : Lock, body: () -> Unit) {
    l.lock()
    try {
        return body()
    }
    finally {
        l.unlock()
    }
}

Java(バイトコードをデコンパイル)

public final class InlineLambdaKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Lock obj = (Lock)(new ReentrantLock());
      obj.lock();

      try {
         // -------------------------------------------
         // 渡したラムダがインライン化されていることを確認できる
         // -------------------------------------------
         String var2 = "Hello";
         System.out.println(var2);
         // -------------------------------------------
      } finally {
         obj.unlock();
      }

   }

   private static final void lock(Lock l, Function0 body) {
      l.lock();

      try {
         body.invoke();
      } finally {
         InlineMarker.finallyStart(1);
         l.unlock();
         InlineMarker.finallyEnd(1);
      }

   }
}

noinline修飾子をつけると引数のラムダはインライン化しない

引数のラムダをインライン化したくない場合もあるかと思います。
そのときはnoinline修飾子をラムダの前につけるとインライン化しないようにできます。

Kotlin

fun main(args: Array<String>): Unit {
    val obj: Lock = ReentrantLock()
    lock(obj) {
        println("Hello")
    }
}

private inline fun lock(l : Lock, noinline body: () -> Unit) {
    l.lock()
    try {
        return body()
    }
    finally {
        l.unlock()
    }
}

Java(バイトコードをデコンパイル)

public final class NoinlineLambdaKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Lock obj = (Lock)(new ReentrantLock());
      Function0 body$iv = (Function0)null.INSTANCE;
      // -----------------------------------------------------------
      // lock関数はインライン化されているが、ラムダはインライン化されていない
      // -----------------------------------------------------------
      obj.lock();

      try {
         body$iv.invoke();
      } finally {
         obj.unlock();
      }
      // -----------------------------------------------------------
   }

   private static final void lock(Lock l, Function0 body) {
      l.lock();

      try {
         body.invoke();
      } finally {
         InlineMarker.finallyStart(1);
         l.unlock();
         InlineMarker.finallyEnd(1);
      }

   }
}

インライン関数では引数のラムダでリターンできる

次のように高階関数の引数のラムダでリターンしたい場合があるとします。
高階関数の場合は"return is not allow here"となりコンパイルエラーとなります。
💡 引数に指定したラムダにリターンを記述し、それを実行している関数からリターンすることを局所的リターンと呼ぶらしい

fun main(args: Array<String>): Unit {
    val obj: Lock = ReentrantLock()

    print("Start")
    
    ordinalyLock(obj) {
        //return  "return is not allow here" となりエラーとなる 
    }
    
    print("End")
}

private fun ordinalyLock(l: Lock, body: () -> Unit) {
    l.lock()
    try {
        return body()
    }
    finally {
        l.unlock()
    }
}

ですがインライン関数であれば引数のラムダでリターンできます。
次のコードのようにEndを出力せずにメイン関数を抜けられるようなコードを書けます。

💡 インライン関数では引数のラムダもインライン化されます、
ラムダに記載したreturnがインライン化されるのでエラーにならないそうです。

fun main(args: Array<String>): Unit {
    val obj: Lock = ReentrantLock()

    println("Start")

    inlineLock(obj) {
        return
    }

    println("End")
}

private inline fun inlineLock(l : Lock, body: () -> Unit) {
    l.lock()
    try {
        return body()
    }
    finally {
        l.unlock()
    }
}

インライン関数の引数のラムダを別のコンテキストで
実行したい場合はcrossinline修飾子を使う

次のようにインライン関数の引数のラムダを別のコンテキスト、
例えばローカルオブジェクトやネスト関数から呼び出そうとしてみます。
そうすると次のコンパイルエラーが発生します、内容を見るとcrossinlineを追加しろとエラーが出ています。

private inline fun nocrossinlineLamda(lambda : () -> Unit) {
    val f = object : Runnable {
        override fun run() {
            //lambda()  //ローカルオブジェクトでは呼べない
        }
    }

    {
        //lambda() //ネストした関数では呼べない
    }.invoke()
}
"Can't inline 'lambda' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'lambda'"

crossinlineを追加していみると確かにコンパイルが通るようになりました。
crossinlineをつければローカルオブジェクトやネスト関数からラムダを呼び出せるようになります。

private inline fun crossinlineLamda(crossinline  lambda: () -> Unit)
{
    lambda() // ここでは呼べる

    val f = object : Runnable {
        override fun run() {
            lambda() // crosslineにしたことで呼べるようになる
        }
    }

    {
        lambda() // corsslineにしたことで呼べるようになる
    }.invoke()
}

まとめ

Kotlinのインラインに関する機能でできることは次の通り!!

  • インライン関数にするにはinline修飾子をつける
  • インライン関数のラムダまでインライン化の対象となる
  • インライン関数のラムダをインライン化したくない場合はnoinline修飾子をつける
  • インライン関数のラムダではリターンすることができる
  • インライン関数のラムダをローカルオブジェクトやネストした関数で呼び出したいときはcrossinline修飾子をつける

参考

76
41
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
41