Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

디지안의 개발일지

[TIL] Inline functions 본문

Kotlin

[TIL] Inline functions

안덕기 2022. 2. 10. 21:23

Inline functions

들어가기 전에

이 글은 코틀린 공식 홈페이지의 글을 보고 배운 내용을 작성하였습니다.

람다식의 문제점

고차함수를 통해서 람다식을 사용하면 Runtime에 패널티가 부과될 수도 있다. 람다식 자체가 하나의 오브젝트고 클로저를 캡처하기 때문이다.

쉽게 말하면 람다식은 문법적으로 함수형 인터페이스의 구상 익명 객체이기 때문에 결과적으로 객체가 되고 람다식을 사용한 만큼 객체가 증가하기 때문에 메모리를 더 사용하게 된다.

Inline 함수

그렇기 때문에 등장한 것이 inline 함수다. inline 함수는 컴파일 시점에 람다식을 마치 원래 그 자리에 해당 블록 구문들이 존재하는 것처럼 변경해준다.

예시로 아래와 같은 코드를 보자.

  • 코틀린 코드
fun main() {
    var a: String
    doSomething {
        a = "b"
        println(a)
    }
}

fun doSomething(body: () -> Unit) {
    body.invoke()
}
  • 코틀린 바이트 코드를 디컴파일한 자바 코드
@Metadata(
   mv = {1, 6, 0},
   k = 2,
   d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\u001a\u0014\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00010\u0003\u001a\u0006\u0010\u0004\u001a\u00020\u0001¨\u0006\u0005"},
   d2 = {"doSomething", "", "body", "Lkotlin/Function0;", "main", "kotlin-study.main"}
)
public final class MainKKt {
   public static final void main() {
      final ObjectRef a = new ObjectRef();
      doSomething((Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            a.element = "b";
            String var1 = (String)a.element;
            System.out.println(var1);
         }
      }));
   }

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

   public static final void doSomething(@NotNull Function0 body) {
      Intrinsics.checkNotNullParameter(body, "body");
      body.invoke();
   }
}

알아볼 수 없는 다른 내용을 제외하고 doSomething을 호출할 때 Function0 클래스를 인스턴스화 하는 것을 확인할 수 있다.

doSomething 함수 앞에서 inline이라는 키워드를 넣고 디컴파일을 해보면 아래와 같이 되는 것을 볼 수 있다.

  • 코틀린 코드
fun main() {
    var a: String
    doSomething {
        a = "b"
        println(a)
    }
}

inline fun doSomething(body: () -> Unit) {
    body.invoke()
}
  • 코틀린 바이트 코드를 디컴파일한 자바 코드
@Metadata(
   mv = {1, 6, 0},
   k = 2,
   d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\u001a\u001a\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00010\u0003H\u0086\bø\u0001\u0000\u001a\u0006\u0010\u0004\u001a\u00020\u0001\u0082\u0002\u0007\n\u0005\b\u009920\u0001¨\u0006\u0005"},
   d2 = {"doSomething", "", "body", "Lkotlin/Function0;", "main", "kotlin-study.main"}
)
public final class MainKKt {
   public static final void main() {
      Object a = null;
      int $i$f$doSomething = false;
      int var2 = false;
      a = "b";
      System.out.println(a);
   }

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

   public static final void doSomething(@NotNull Function0 body) {
      int $i$f$doSomething = 0;
      Intrinsics.checkNotNullParameter(body, "body");
      body.invoke();
   }
}

Function0 클래스를 인스턴스화 하던 부분은 없어지고 System.out.println(a)이 호출되는 것을 볼 수 있다.

noinline

반대로 inline 함수의 일부 람다식을 inline 하지 않게 할 수도 있다.

  • 코틀린 코드
fun main() {
    var a: String
    doSomething ({
        a = "b"
        println(a)
    }){
        a = "c"
        println(a)
    }
}

inline fun doSomething(body: () -> Unit, notInlined: () -> Unit) {
    body.invoke()
    notInlined.invoke()
}
  • 코틀린 바이트 코드를 디컴파일한 자바 코드
@Metadata(
   mv = {1, 6, 0},
   k = 2,
   d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\u001a(\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00010\u00032\f\u0010\u0004\u001a\b\u0012\u0004\u0012\u00020\u00010\u0003H\u0086\bø\u0001\u0000\u001a\u0006\u0010\u0005\u001a\u00020\u0001\u0082\u0002\u0007\n\u0005\b\u009920\u0001¨\u0006\u0006"},
   d2 = {"doSomething", "", "body", "Lkotlin/Function0;", "notInlined", "main", "kotlin-study.main"}
)
public final class MainKKt {
   public static final void main() {
      Object a = null;
      int $i$f$doSomething = false;
      int var2 = false;
      a = "b";
      System.out.println(a);
      var2 = false;
      a = "c";
      System.out.println(a);
   }

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

   public static final void doSomething(@NotNull Function0 body, @NotNull Function0 notInlined) {
      int $i$f$doSomething = 0;
      Intrinsics.checkNotNullParameter(body, "body");
      Intrinsics.checkNotNullParameter(notInlined, "notInlined");
      body.invoke();
      notInlined.invoke();
   }
}

람다식이 풀리면서 코드에 삽입된 것을 확인할 수 있다.

람다식 중 두번째 파라미터에 noinline을 넣으면 어떻게 되는지 보자.

  • 코틀린 코드
fun main() {
    var a: String
    doSomething ({
        a = "b"
        println(a)
    }){
        a = "c"
        println(a)
    }
}

inline fun doSomething(body: () -> Unit, noinline notInlined: () -> Unit) {
    body.invoke()
    notInlined.invoke()
}
  • 코틀린 바이트 코드를 디컴파일한 자바 코드
@Metadata(
   mv = {1, 6, 0},
   k = 2,
   d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\u001a*\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00010\u00032\u000e\b\b\u0010\u0004\u001a\b\u0012\u0004\u0012\u00020\u00010\u0003H\u0086\bø\u0001\u0000\u001a\u0006\u0010\u0005\u001a\u00020\u0001\u0082\u0002\u0007\n\u0005\b\u009920\u0001¨\u0006\u0006"},
   d2 = {"doSomething", "", "body", "Lkotlin/Function0;", "notInlined", "main", "kotlin-study.main"}
)
public final class MainKKt {
   public static final void main() {
      final ObjectRef a = new ObjectRef();
      Function0 notInlined$iv = (Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            a.element = "c";
            String var1 = (String)a.element;
            System.out.println(var1);
         }
      });
      int $i$f$doSomething = false;
      int var3 = false;
      a.element = "b";
      String var4 = (String)a.element;
      System.out.println(var4);
      notInlined$iv.invoke();
   }

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

   public static final void doSomething(@NotNull Function0 body, @NotNull Function0 notInlined) {
      int $i$f$doSomething = 0;
      Intrinsics.checkNotNullParameter(body, "body");
      Intrinsics.checkNotNullParameter(notInlined, "notInlined");
      body.invoke();
      notInlined.invoke();
   }
}

해석하기 매우 어렵지만 그 이전과 다르게 Function0 클래스를 인스턴스화 하는 것을 확인할 수 있다.

noline을 쓰는 경우는 여러 경우가 있겠지만 그 중 한가지 예가 람다식을 풀어줄 수 없을 때이다. 예를 들어 아래와 같은 코드가 있을 때 람다식을 메모리에 저장해야되기 때문에 noinline을 반드시 작성해줘야한다.

fun main() {
    test({}, {})
}

inline fun test(a:()->Unit, noinline b:()->Unit){
    a()
    val map = mutableMapOf<String, () -> Unit>()
    map["test"] = b
}

또 주의해야할 점은 인라인 함수에 넣은 람다식이 만약 인라인을 할 수 없는 구조로 구성이 되어있다면 인라인으로 풀어낼 수가 없다. 그러므로 그 부분은 조심해서 사용해야한다.

Non-local returns

일반적으로 아래와 같이 람다식 내부적으로 return이 있으면 컴파일 에러가 발생한다.

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach2 { 
        if(it == 0) return true
    }
    return false
}

fun <T> Iterable<T>.forEach2(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

코틀린 컬렉션 함수는 다음과 같이 inline으로 선언이 되어있는데 inline 함수를 사용하면 람다식 자체가 컴파일시 함수에 포함되기 때문에 return 문을 작성할 수 있다. 좀 더 편리한 이점이 있는 것을 알 수 있다.

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if(it == 0) return true
    }
    return false
}

만약 람다식 내부적으로 return을 사용하는 것을 허락하지 않고 싶다면 crossline 이라는 키워드를 넣어주면 된다.

inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body()
    }
    // ...
}

Reified type parameters

제네릭 함수에서는 기본적으로 T 타입이 컴파일 시점에서 타입을 알 수 있지만 런타임 시점에서는 Type Eraser에 의해서 그 타입이 삭제된다.

제네릭은 Java 5부터 나온 기술인데 Java 4를 호환하기 위해서 제네릭 타입에 대해서는 타입을 없애버린 것이라고 한다.

inline 함수에서 제네릭 타입을 쓸 때, 타입을 명시하면서 간단하게 사용하고 싶으면 Reified 라는 키워드를 추가하면 된다.

  • reified를 사용하지 않는 경우
fun <T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}

아래와 같은 에러가 뜨는 것을 확인할 수 있다.

Cannot use 'T' as reified type parameter. Use a class instead.

타입 T에 대해서 타입을 명시하지 않았기 때문에 class를 사용할 수 없다는 의미다.

  • reified를 사용한 경우
inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}
  • 디컴파일한 Java 코드
@Metadata(
   mv = {1, 6, 0},
   k = 2,
   d1 = {"\u0000 \n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\n\u0002\u0010\u001e\n\u0002\u0018\u0002\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005\u001a\u001b\u0010\u0006\u001a\f\u0012\b\u0012\u0006\u0012\u0002\b\u00030\b0\u0007\"\u0006\b\u0000\u0010\t\u0018\u0001H\u0086\b¨\u0006\n"},
   d2 = {"main", "", "s", "", "", "([Ljava/lang/String;)V", "membersOf", "", "Lkotlin/reflect/KCallable;", "T", "kotlin-study.main"}
)
public final class MainKKt {
   // $FF: synthetic method
   public static final Collection membersOf() {
      int $i$f$membersOf = 0;
      Intrinsics.reifiedOperationMarker(4, "T");
      return Reflection.getOrCreateKotlinClass(Object.class).getMembers();
   }

   public static final void main(@NotNull String[] s) {
      Intrinsics.checkNotNullParameter(s, "s");
      int $i$f$membersOf = false;
      String var2 = CollectionsKt.joinToString$default((Iterable)Reflection.getOrCreateKotlinClass(StringBuilder.class).getMembers(), (CharSequence)"\n", (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 62, (Object)null);
      System.out.println(var2);
   }
}

중간에 StringBuilder 클래스에 대한 정보가 넘어간 것을 확인할 수 있다.