[Kotlin] Inline Function 파헤치기

haero_kim·2021년 10월 12일
3

Kotlin 과 친해지기

목록 보기
14/16
post-thumbnail

💡 해당 포스팅은 High-Order Function 에 대한 이해를 필요로 합니다.

코틀린에서는 고차함수를 사용할 수 있다. 함수(람다)를 함수의 호출 인자로 전달하거나, 반환값으로 활용할 수도 있다. 그런데 이렇게 람다를 사용하게 되면, 부가적인 메모리 할당으로 인해 메모리 효율이 안 좋아지고, 함수 호출로 인한 런타임 오버헤드가 발생하게 된다. 잘 와닿지 않는가? 한 번 아래 예시를 통해 왜 부가적으로 메모리가 할당되고 런타임 오버헤드가 발생하는지에 대해 알아보자.


Lambda 를 사용하는 경우

아래 코드에서는 파라미터로 정수형 데이터와 람다식을 받는 someMethod() 가 있고, 내부적으로 람다를 호출한 뒤 전달받은 정수를 2배 늘려 반환하는 형태의 동작을 하게 된다.

fun someMethod(a: Int, doSomething: () -> Unit): Int {
    doSomething()
    return 2 * a
}

fun main() {
    val result = someMethod(10) {
        println("ㅇㅅㅇ")
    }
    println(result)
}

이러한 코드의 경우 컴파일 시 어떠한 Java 코드로 변환되게 될까?

public final class LambdaFunctions {
   public static final LambdaFunctions INSTANCE;

   public final int someMethod(int a, @NotNull Function0 doSomething) {
      doSomething.invoke();
      return 2 * a;
   }

   @JvmStatic
   public static final void main(@NotNull String[] args) {
      int result = INSTANCE.someMethod(2, (Function0)null.INSTANCE);
      System.out.println(result);
   }

   static {
      LambdaFunctions var0 = new LambdaFunctions();
      INSTANCE = var0;
   }
}

눈치챈 사람도 있겠지만, doSomethine람다식을 전달해주도록 구현한 부분이 새로운 객체를 생성하여 넘겨주는 식으로 변환되는 것을 확인할 수 있다. 그리고 넘긴 객체를 통해 함수 호출을 하도록 구현되어 있다. 이렇게 되면 무의미하게 객체를 생성하여 메모리를 차지하고, 내부적으로 연쇄적인 함수 호출을 하게 되어 오버헤드가 발생하여 성능이 떨어질 수 있다.

따라서, 이러한 방식을 사용하는 것이 아닌 람다식 내부의 실행문(코드)들을 컴파일 시 람다를 호출하는 부분에 주입하는 방식을 사용하는 것이 바로 Inline Function 의 개념이다.


Inline Function

어떻게 쓸까? 그냥 냅다 inline 키워드를 붙이면 된다.

inline fun someMethod(a: Int, doSomething: () -> Unit): Int {
    doSomething()
    return 2 * a
}

fun main() {
    val result = someMethod(10) {
        println("ㅇㅅㅇ")
    }
    println(result)
}

키워드 하나를 추가했을 뿐인데, 아래와 같이 컴파일되는 형태가 달라진다.

public final class InlineFunctions {

   @JvmStatic
   public static final void main(@NotNull String[] args) {
	     int a = 2;
       int var5 = false;
       String var6 = "ㅇㅅㅇ";
       System.out.println(var6);
       int result = 2 * a;
       System.out.println(result);
   }

}

설명한대로, 람다를 호출하는 부분에 람다식 내부의 코드가 그대로 복사된 것을 확인해볼 수 있다. 컴파일되는 바이트코드 양은 더 늘어나겠지만, 객체를 생성하거나 함수를 또 호출하는 등 비효율적인 행동은 하지 않는다.

이러한 이유로 인라인 함수는 일반 함수보다 성능이 좋다.

🤚🏻 하지만 내부적으로 코드를 복사하는 개념이기 때문에, 인자로 전달받은 함수는 다른 함수로 전달되거나 참조될 순 없다.

따라서 아래와 같은 코드는 동작하지 않는다. 왜냐하면 전달받은 함수를 다른 함수로 넘겨주고 있기 때문이다. 이는 컴파일 에러를 발생하게 된다.

inline fun firstMethod(a: Int, func1: () -> Unit, func2: () -> Unit) {
    func1()
    secondMethod(10, func2)
}

fun secondMethod(a: Int, func: () -> Unit): Int {
    func()
    return 2 * a
}

fun main() {
    firstMethod(2, {
        println("Just some dummy function")
    }, {
        println("can't pass function in inline functions")
    })
}

그렇다면, 이런 경우에는 어떻게 해야할까?

라고 고민하는 우리를 위해 noinline 이라는 키워드를 제공해준다.


noinline Keyword

전달받은 함수들 중 일부는 다른 함수로 넘겨줘야할 때와 같이, 모든 인자를 inline 처리해서는 안 될 때가 있다. 이럴 때 사용하는 키워드가 바로 noinline 이다. inline 에서 제외시킬 인자 앞에 noinline 키워드를 붙이면 된다. 그 순간 이후로 해당 인자는 다른 함수로 전달할 수 있다.

inline fun firstMethod(a: Int, func1: () -> Unit, noinline func2: () -> Unit) {
    func1()
    secondMethod(10, func2)
}

fun secondMethod(a: Int, func: () -> Unit): Int {
    func()
    return 2 * a
}

fun main() {
    firstMethod(2, {
        println("Just some dummy function")
    }, {
        println("can't pass function in inline functions")
    })
}

이를 자바로 변환하게 되면, 아래와 같은 모양을 갖는다.

public final void firstMethod(int a, @NotNull Function0 func, @NotNull Function0 func2) {
   func.invoke();
   this.secondMethod(10, func2);
}

public final int secondMethod(int a, @NotNull Function0 func) {
   func.invoke();
   return 2 * a;
}

@JvmStatic
public static final void main(@NotNull String[] args) {
   String var6 = "Just some dummy function";
   System.out.println(var6);
   this_$iv.secondMethod(10, func2$iv);
}

코드를 보면 func2() 를 제외한 나머지 코드들은 인라인 처리가 되었고, func2() 는 기존 방식대로 객체를 새로 생성하여 호출되는 것을 확인할 수 있다.


그럼 항상 inline 이 정답인가?

일반 함수보단 성능 좋은 거 알겠다. 그렇다면 항상 inline 으로 사용하면 되는거잖아? 라고 생각할 수 있다. 그러나 많은 코드를 갖고 있는 람다를 inline 처리하면 바이트코드의 양이 훨씬 많아지게 된다. 이 경우 성능이 오히려 악화될 수도 있다. 따라서 inline 처리는 1~3줄 정도의 길이를 권장하고 있다.


이렇듯 Inline Function 은 적절히 잘 활용한다면, 성능에 매우 좋은 영향을 끼칠 수 있다.

참고자료

https://medium.com/android-news/learning-kotlin-inline-functions-18a94d3efe46
https://codechacha.com/ko/kotlin-inline-functions/

profile
어려울수록 기본에 미치고 열광하라

0개의 댓글