💡 해당 포스팅은 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 int someMethod(int a, @NotNull Function0 doSomething) {
Intrinsics.checkNotNullParameter(doSomething, "doSomething");
doSomething.invoke();
return 2 * a;
}
public static final void main() {
int result = someMethod(10, (Function0)null.INSTANCE);
System.out.println(result);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
눈치챈 사람도 있겠지만, doSomethine 에 람다식을 전달해주도록 구현한 부분이 새로운 객체를 생성하여 넘겨주는 식으로 변환되는 것을 확인할 수 있다. 그리고 넘긴 객체를 통해 함수 호출을 하도록 구현되어 있다. 이렇게 되면 무의미하게 객체를 생성하여 메모리를 차지하고, 내부적으로 연쇄적인 함수 호출을 하게 되어 오버헤드가 발생하여 성능이 떨어질 수 있다.
정리하면, inline 함수로 정의된 함수는 컴파일 단계에서 함수를 호출하는 코드 대신에, 함수 본문 코드 자체가 삽입되는 방식이라고 할 수 있다.
인라인(Inline) 함수
어떻게 쓸까? 함수 앞에 inline 키워드를 붙이면 된다.
그렇게하면, 그 함수의 본문이 호출 지점에 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 {
public static final int someMethod(int a, @NotNull Function0 doSomething) {
int $i$f$someMethod = 0;
Intrinsics.checkNotNullParameter(doSomething, "doSomething");
doSomething.invoke();
return 2 * a;
}
public static final void main() {
int a$iv = 10;
int $i$f$someMethod = false;
int var3 = false;
String var4 = "ㅇㅅㅇ";
System.out.println(var4);
int result = 2 * a$iv;
System.out.println(result);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
설명한대로, 람다를 호출하는 부분에 람다식 내부의 코드가 그대로 복사된 것을 확인해볼 수 있다.
따라서 Inline함수를 사용하면, 객체를 생성하거나 함수를 또 호출하는 등 비효율적인 행동은 하지 않는다.
이러한 이유로 인라인 함수는 일반 함수보다 성능이 좋다.
하지만, 장점만 있는 것은 아니다.
컴파일되는 바이트코드 양은 Inline함수를 사용하기 전 보다 더 늘어나는 문제가 존재한다.
실제로, someMethod 객체를 하나 더 생성해보면 바이트 코드량은 더 늘어난다.
inline fun someMethod(a: Int, doSomething: () -> Unit): Int {
doSomething()
return 2 * a
}
fun main() {
val result1 = someMethod(10) {
println("ㅇㅅㅇ")
}
val result2 = someMethod(11) {
println("ㅇㅅㅇ")
}
println(result1)
println(result2)
}
Java로 디컴파일 한 결과는 아래와 같다.
public final class LambdaFunctions {
public static final int someMethod(int a, @NotNull Function0 doSomething) {
int $i$f$someMethod = 0;
Intrinsics.checkNotNullParameter(doSomething, "doSomething");
doSomething.invoke();
return 2 * a;
}
public static final void main() {
int result2 = 10;
int $i$f$someMethod = false;
int $i$f$someMethod = false;
String var4 = "ㅇㅅㅇ";
System.out.println(var4);
int result1 = 2 * result2;
int a$iv = 11;
$i$f$someMethod = false;
int var7 = false;
String var5 = "ㅇㅅㅇ";
System.out.println(var5);
result2 = 2 * a$iv;
System.out.println(result1);
System.out.println(result2);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
이러한 이유로, Inline함수가 항상 옳은 대안은 아니다. 많은 코드를 갖고 있는 람다를 inline 처리하면 바이트코드의 양이 훨씬 많아지게 된다.
이 경우 앱 크기가 증가하는 등 성능이 오히려 악화될 수도 있다. 따라서 inline 처리는 1~3줄 정도의 길이를 권장하고 있다.
실제로 우리가 자주 사용하는 filter, map 등등도 inline 함수로 구현되어 있는데 매우 짧은 코드로 구성되어 있음을 확인할 수 있다.
또한 inline함수는 내부적으로 내부적으로 코드를 복사하는 개념이기 때문에, 인자로 전달받은 함수는 다른 함수로 전달되거나 참조될 수는 없다.
따라서 아래와 같은 코드는 동작하지 않는다. 왜냐하면 전달받은 함수를 다른 함수로 넘겨주고 있기 때문이다. 이는 컴파일 에러를 발생하게 된다.
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 Function 은 적절히 잘 활용한다면, 성능에 매우 좋은 영향을 끼칠 수 있다.
어떤 경우에 Inline 함수를 사용하면 좋을까?
결론 부터 말하면, 람다를 파라미터로 받으며, 코드 크기가 작은 함수의 경우 inline을 적용 하는 것이 유리하다.
일반적인 함수 호출은 JVM에서 JIT(Just-In-Time) 컴파일러가 최적화를 수행하며, 이 과정에서 자동으로 인라이닝이 이루어지기 때문에 개발자가 굳이 inline 키워드를 사용할 필요가 없다.
하지만 고차 함수, 즉 람다를 인자로 받는 함수의 경우에는 상황이 조금 다르다.
람다를 넘기면 그 람다는 익명 클래스 객체로 변환되고, 객체 생성 비용이 추가되는데, inline 키워드를 사용하면 람다도 함께 인라이닝되어 이러한 불필요한 객체 생성을 줄일 수 있다.
단, inline 함수를 만들 때 코드 크기가 큰 함수의 경우는 모든 호출 지점에 바이트코드가 복사되기 때문에 오히려 더 성능을 악화시킬 수 있기 때문에 가급적이면 코드 크기가 작은 부분에만 inline 함수를 사용하면 좋을 것 같다.
정리
- 기본적으로 JVM의 JIT 컴파일러에 의해서 일반 함수들은 inline 함수를 사용했을 때 더 좋다고 생각되어지면 JVM이 자동으로 만들어주고 있다.
- 위에서 변환된 자바 바이트 코드를 보면 길이가 더 길어져 너무 긴 메소드에 사용시 메모리가 낭비 될 수있다.
- inline keyword는 1~3줄 정도 길이의 함수에 사용하는 것이 효과적일 수 있다.
- 필요시 특정 메소드를 인라인 방식에서 제외 하고 싶다면 noinline을 사용하자.
끝.
참고자료
https://0391kjy.tistory.com/64
코틀린(Kotlin) - inline 함수 : 람다의 부가 비용 없애기
람다의 경우 컴파일 단계에서 파라미터 개수에 따라 FunctionN 형태의 인터페이스로 변환이 됩니다. 예를 들어 아래와 같이 파라미터가 두 개인 람다 식은 Function2 의 인터페이스로 변환이 되는 것
0391kjy.tistory.com
https://codechacha.com/ko/kotlin-inline-functions/
Kotlin - inline functions 이해하기
inline functions는 함수 내용을 호출하는 부분에 복사하여 추가적인 메모리 할당이나 함수 호출로 발생하는 Runtime overhead를 줄여줍니다. noinline 키워드는 특정 인자만 제외하고 나머지만 inlnie으로
codechacha.com
https://velog.io/@haero_kim/Kotlin-Inline-Function-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0
[Kotlin] Inline Function 파헤치기
자바에는 없는 강력한 기능, Inline Function
velog.io
'Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린의 data class는 왜 상속이 불가능할까? (0) | 2025.03.29 |
---|---|
[Kotlin] Nested Class와 Inner Class (0) | 2025.02.27 |
[Kotlin] 코틀린(Kotlin)에서 동시성 문제를 해결하는 방법 (0) | 2025.02.13 |
[Kotlin] 불변성(Immutable)과 가변성(Mutable) (5) | 2024.11.07 |
[Kotlin] 확장(Extension) 함수 (5) | 2024.11.06 |