💡 해당 포스팅은 High-Order Function 에 대한 이해를 필요로 합니다.
코틀린에서는 고차함수를 사용할 수 있다. 함수(람다)를 함수의 호출 인자로 전달하거나, 반환값으로 활용할 수도 있다. 그런데 이렇게 람다를 사용하게 되면, 부가적인 메모리 할당으로 인해 메모리 효율이 안 좋아지고, 함수 호출로 인한 런타임 오버헤드가 발생하게 된다. 잘 와닿지 않는가? 한 번 아래 예시를 통해 왜 부가적으로 메모리가 할당되고 런타임 오버헤드가 발생하는지에 대해 알아보자.
아래 코드에서는 파라미터로 정수형 데이터와 람다식을 받는 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
키워드를 붙이면 된다.
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/
안녕하세요 공부하다가 좋은 글 읽고갑니다! 제가 궁금한게 있는데 인라인 함수가 실제로 안드로이드 개발 업무에서 자주 쓰이나요? 아직 공부단계라 그런지 지금으로선 와 이걸 어떻게 적용하지.. 라는 생각이 들어서요