기타/코틀린

[Kotlin] inline과 reified

창이2 2022. 7. 2. 12:56

안녕하세요.

이번 포스팅에서는 Inline과 reified 키워드에 대해 알아보려고 합니다.

 

inline 키워드는 함수에 붙는 키워드로 많이 볼수가 있는데요.

가령 흔히 쓰이는 Collection 함수에서 forEach 문을 보면 아래처럼 inline을 사용하는 것을 볼 수 있습니다.

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

 

inline 이란 뜻을 그대로 해석해보면 line 안에 라는 뜻으로 해석할 수 있는데

이말은 즉 코드를 라인안에 넣는다는 의미로 해석하시면 될 거같습니다.

 

우리가 고차함수(함수를 parameter로 넣거나 결과를 반환하는 함수)를 사용할때 람다식을 많이 사용하는데 이 람다식에 대한

함수를 새로 생성하느냐 아니면 람다식의 코드를 그대로 넣느냐를 결정하는게 inline 키워드의 핵심이라고 보시면 되겠습니다.

 

구체적으로 살펴보면

inline을 붙였을 때와 안붙였을때 decompile 하여 코드를 봤을 때

inline이 붙은 함수는 코드가 그대로 들어가고 inline이 붙지 않은 함수는 함수 인스턴스를 생성하서 수행한다는 것을 알 수 있습니다.

 

즉 noInline 함수는 함수 인스턴스에 대한 메모리할당, virtual call로 인한 런타임 오버헤드를 높이는 것으로 이해하시면 됩니다. 

 

   public static final void main() {
      List list = CollectionsKt.emptyList();
      Iterable $this$inlineForEach$iv = (Iterable)list;
      int $i$f$inlineForEach = false;
      Iterator var3 = $this$inlineForEach$iv.iterator();

      while(var3.hasNext()) {
         Object element$iv = var3.next();
         String it = (String)element$iv;
         int var6 = false;
         System.out.println(it);
      }

      noInlineForEach((Iterable)list, (Function1)null.INSTANCE);
   }

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

   public static final void inlineForEach(@NotNull Iterable $this$inlineForEach, @NotNull Function1 action) {
      int $i$f$inlineForEach = 0;
      Intrinsics.checkNotNullParameter($this$inlineForEach, "$this$inlineForEach");
      Intrinsics.checkNotNullParameter(action, "action");
      Iterator var4 = $this$inlineForEach.iterator();

      while(var4.hasNext()) {
         Object element = var4.next();
         action.invoke(element);
      }

   }

   public static final void noInlineForEach(@NotNull Iterable $this$noInlineForEach, @NotNull Function1 action) {
      Intrinsics.checkNotNullParameter($this$noInlineForEach, "$this$noInlineForEach");
      Intrinsics.checkNotNullParameter(action, "action");
      Iterator var3 = $this$noInlineForEach.iterator();

      while(var3.hasNext()) {
         Object element = var3.next();
         action.invoke(element);
      }

   }

 

함수를 inline하면 여러가지 장점이 몇개 있는데

위에서 설명드린 런타임 오버헤드가 줄어드는것과 람다식안에 return 과 같은 함수 종료문도 쓸수 있게 됩니다.

왜냐하면 디컴파일 될때 같은 local control flow(같은 함수 범위) 을 타고 있기 때문입니다.

 

inline기능을 사용하면서 local control flow 을 막으려면 crossInline 키워드를 사용하면 됩니다. 

 

또한 만약 람다로 들어가는 paramter가 여러개일때 특정 람다식에 대해서 noInline을 사용하면 디컴파일 때 inline 적용이 안되게 할 수 있습니다. 가령 람다식을 변수로 취해서 사용하는 경우 noInline을 붙이면 되겠습니다.

 

위에서 보면 Inline이 완벽해 보이나 Inline에도 단점이 존재합니다.

1. private일때 사용이 불가능

2. 바이트 코드 길이가 증가하는 것

 

결과적으로 inline은 open 함수이면서 길이가 짧은 코드에 적용하는게 효과적이라 볼 수 있습니다.

 

다음은 Inline에 대한 예제 코드입니다.

 

더보기
fun main() {

    val list = listOf<String>()

    list.inlineForEach(
        inlineAction = {
            if (it.isEmpty()) return
            println(it)
        },
        crossInlineAction = {
            //Error if(it.isEmpty()) return
            println(it)
        },
        noInlineAction = {
            //Error if (it.isEmpty()) return
            println(it)
        }
    )

    list.noInlineForEach {
        println(it)
    }

}

inline fun <T> Iterable<T>.inlineForEach(
    inlineAction: (T) -> Unit,
    crossinline crossInlineAction: (T) -> Unit,
    noinline noInlineAction: (T) -> Unit
): Unit {
    for (element in this) {
        inlineAction(element)
        noInlineAction(element)
        crossInlineAction(element)
    }
}

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

 

 

 

다음은 reified에 대해 알아보겠습니다.

refied는 inline 함수에서 사용할 수 있는 키워드입니다.

generic type은 일반적인 경우에  reified type parameter 로 사용할 수 없습니다.

즉 구체화된 타입이 지정되어 있지 않기 때문에 reified type 을 따로 넘겨주거나 하는 불편한 코드가 들어가게 됩니다.

이때 inline함수에 reified를 사용하면 이 문제를 해결 할 수 있습니다.

 

fun <T> noInlineGenericTest(data: T) {

    when (T::class) { //T는 reified type parameter 사용할 수 없다.
        Int::class -> {
            println("noInlineGenericTest Int: $data")
        }
        String::class -> {
            println("noInlineGenericTest String: $data")
        }
    }

}

inline fun <reified T> inlineGenericTest(data: T) {

    when (T::class) { //코드가 그대로 들어가기 때문에
        Int::class -> {
            println("inlineGenericTest Int: $data")
        }

        String::class ->{
            println("inlineGenericTest String: $data")
        }
    }

}

 

아래는 50줄의 함수 코드를 inline으로 변경했을때 50개일때와 500줄일때 바이트코드 비교 화면입니다.

즉 50줄짜리 inline함수를 10번 호출하면 4배 정도 크기가 증가한 것으로 볼 수 있습니다. 다만 

row의 크기에 따라 파일 크기가 달라질 수 있습니다.

 

1번 호출했을때

   

 

 

 

 

 

 

 

10번 호출했을때