Kotlin/Variable and Function

[Kotlin] inline fun 함수를 사용한 고차 함수 최적화

Dev.Cho 2024. 4. 2. 07:05

함수의 매개 변수로 람다식을 받을 경우의 문제

일반적으로 함수를 호출하면 해당 함수가 서브루틴으로써 실행된다. 반면 inline fun으로 선언된 함수를 호출하면, 함수 호출을 실행하는 것이 아니라 해당 함수가 호출된 위치에 함수 내부의 코드가 삽입돼 실행된다.

 

예를 들어 다음과 같은 코드가 있다고 해보자. 

fun main(args: Array<String>) {
  printWorldAfterFunction {
    println("Hello")
  }
}

fun printWorldAfterFunction(function: () -> Unit) {
  function()
  println("World")
}

 

이 코드에서 printlnWorldAfterFunction 함수를 () -> Unit 타입의 람다식과 함께 실행하면, () -> Unit 타입의 람다식은 익명 클래스로 만들어져 인스턴스화된다. 이를 확인하기 위해 한 번 위 코드를 바이트 코드로 변환한 다음 다시 자바로 디컴파일 함으로써 확인해 보자. 디컴파일 해보면, 다음과 같은 코드가 나온다.

public final class InlineFunKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      printWorldAfterFunction((Function0)null.INSTANCE);
   }

   public static final void printWorldAfterFunction(@NotNull Function0 function) {
      Intrinsics.checkNotNullParameter(function, "function");
      function.invoke();
      String var1 = "World";
      System.out.println(var1);
   }
}

 

우리가 입력한 function 파라미터는 Function0를 구현하는 익명 클래스로 만들어지며, 이 클래스의 인스턴스가 printWorldAfterFunction에 전달됨을 확인할 수 있다.

*여기서는 null.INSTANCE로 표시되지만 실제로는 Fuction0는 invoke 함수 호출 시 { println("Hello") }  실행하는 객체이다.

 

즉, 함수의 인자로 람다식을 사용하면 새로운 클래스의 인스턴스가 만들어진다. 매번 함수가 호출될 때마다 익명 클래스의 새로운 인스턴스가 메모리 상에 올라가는 것은 성능에 좋지 않다.

 

inline fun을 사용한 성능 최적화

위와 같은 문제를 해결하기 위해 inline fun이 등장한다. 위의 printWorldAfterFunction을 inline fun으로 선언해보자.

fun main(args: Array<String>) {
  printWorldAfterFunction {
    println("Hello")
  }
}

inline fun printWorldAfterFunction(function: () -> Unit) {
  function()
  println("World")
}

 

이제 위의 코드를 다시 자바로 디컴파일 해보자. 그러면 다음과 같은 코드가 나오는 것을 볼 수 있다.

public final class InlineFunKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      int $i$f$printWorldAfterFunction = false;
      int var2 = false;
      String var3 = "Hello"; 
      System.out.println(var3); // 그대로 코드로 대입
      String var4 = "World";
      System.out.println(var4); // 그대로 코드로 대입
   }

   public static final void printWorldAfterFunction(@NotNull Function0 function) {
      int $i$f$printWorldAfterFunction = 0;
      Intrinsics.checkNotNullParameter(function, "function");
      function.invoke();
      String var2 = "World";
      System.out.println(var2);
   }
}

 

이 코드에서 main 함수는 더 이상 printWorldAfterFunction 함수를 호출하지 않는 것을 볼 수 있다. 또한 더이상 Function0를 구현하는 익명 클래스와 그 인스턴스가 만들어지지 않는다. 대신 printWorldAfterFunction의 코드와 우리가 function이라는 파라미터로 넘겨준 람다식이 그대로 코드로 입력된 것을 볼 수 있다.

이렇게 하면, 코드는 약간 길어지겠지만 익명 객체의 인스턴스를 새로 생성하지 않는다. 이를 통해 익명 클래스와 그 인스턴스를 생성하지 않게 돼 최적화가 가능해진다.

 

 

inline fun을 사용해도 의미 없는 경우

참고로, 람다식을 인자로 받지 않는 함수는 inline fun으로 선언해도 별 의미가 없다. 예를 들어 다음과 같은 함수를 보자.

inline fun printString(string: String) {
  println(string)
}

 

이 함수는 람다식을 인자로 받지 않기 때문에 성능상으로 의미가 없다. 따라서 다음과 같은 Warning 메시지가 IDE에서 출력되는 것을 볼 수 있다.

그림1. inline fun 의미가 없는 경우

 

Expected performance impact from inlining is insignificant.
inline 함수를 사용하는 효과가 불명확하다

 

 

반응형