본문 바로가기
기타/코틀린

[Kotlin] inline과 reified

by 창이2 2022. 7. 2.

안녕하세요.

이번 포스팅에서는 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번 호출했을때

댓글