탐험 개요
고차함수를 활용하기 위해 컬렉션 함수 내부 코드를 보던 중 inline을 발견!
inline이 뭐지 코드 줄을 안으로 뭐 하는건가???.. 일단 코틀린에서 제공하는 고차함수 API에서 쓰이는 것으로 확인되니 장점이 있겠지??
그래서 inline이 뭔데?
코틀린 공식문서를 보면 다음과 같이 설명한다.
"고차함수를 사용하면, 부가적인 메모리 할당으로 인해 메모리 효율이 안 좋아지고, 함수 호출로 인한 런타임 오버헤드가 발생하게된다. 그러나 람다식을 inline으로 처리하면 이러한 종류의 오버헤드를 제거할 수 있다."
런타임 오버헤드가 뭔데?
런타임 오버헤드는 프로그램이 실행되는 동안 추가적으로 발생하는 비용이나 부담을 의미한다. 이는 프로그램이 실행되는 동안 발생하는 여러 가지 작업들에 대한 처리 시간이나 자원 소비 등을 포함한다.
아무튼..
고차함수를 사용하면 메모리 효율이 안좋아지고 런타임 오버헤드가 발생하는데, 이것을 inline 키워드를 통해 해결할 수 있다는 것이다.
코드를 보면서 이야기해보자.
고차함수
fun function(
n: Int,
action: () -> Unit,
): Int {
action()
return n
}
fun main() {
val result =
function(10) {
println("고차함수")
}
println(result)
}
위 코드를 보게되면 function이라는 고차함수가 있고, 이 고차함수는 정수형 데이터와 람다식 총 2개의 파라미터가 있다.
위 고차함수는 action인 람다식을 실행시키고 n을 반환한다. 얼추 코드를 이해했다면 자바 코드로 디컴파일을 해보자.
고차함수 디컴파일
public final class ApplicationKt {
public static final int function(int n, @NotNull Function0 action) {
Intrinsics.checkNotNullParameter(action, "action");
action.invoke();
return n;
}
public static final void main() {
int result = function(10, (Function0)null.INSTANCE);
System.out.println(result);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
코드를 보게되면 action에 람다식을 전달해주도록 구현한 부분이 새로운 객체를 생성하여 넘겨주고, 넘긴 객체를 통해 함수 호출을 하도록 구현되어 있는 것을 확인할 수 있다.
이는 무의미하게 새로운 객체를 매번 생성하게 된다. 결국 많은 메모리를 차지하게 되고, 내부적으로 연쇄적인 함수 호출을 하게 되어 오버헤드가 발생하여 성능이 떨어질 수 있다.
위 탐험을 통해 고차함수의 단점을 이해할 수 있었다.
inline 고차함수
inline fun function(
n: Int,
action: () -> Unit,
): Int {
action()
return n
}
fun main() {
val result =
function(10) {
println("고차함수")
}
println(result)
}
그렇다면 inline 키워드를 붙인 후 디컴파일을 해보자!
inline 고차함수 디컴파일
public final class ApplicationKt {
public static final int function(int n, @NotNull Function0 action) {
int $i$f$function = 0;
Intrinsics.checkNotNullParameter(action, "action");
action.invoke();
return n;
}
public static final void main() {
int n$iv = 10;
int $i$f$function = false;
int var3 = false;
String var4 = "고차함수";
System.out.println(var4);
System.out.println(n$iv);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
inline 키워드만 붙였을 뿐인데, 위와 같이 컴파일되는 형태가 달라진다.
action()을 호출하는 부분에 람다식 내부의 코드가 그대로 복사된 것을 확인할 수 있다. 컴파일되는 바이트코드 양은 더 늘어나겠지만, 객체를 생성하거나 함수를 또 호출하는 등 비효율적인 행동은 하지 않는다.
이러한 이유로 인라인 함수는 일반 함수보다 성능이 좋다. 인라인 함수를 사용하게 되면 코드는 객체를 항상 새로 만드는것이 아니라 해당 함수의 내용을 호출한 함수에 넣는 방식으로 컴파일 코드를 작성하게 된다.
그렇다면 inline 함수에서 받은 람다식을 다른 함수로 전달하면 어떻게 될까??
컴파일 에러가 발생한다.
내부적으로 코드를 복사하는 개념이기 때문에, 인자로 전달받은 람다식은 다른 함수로 전달되거나 참조될 수 없다.
그런데.. 또.. 이러한 경우에 사용할 수 있는 키워드를 코틀린에서는 제공해준다..
바로.. noinline이다!
noinline은 뭔데?
코틀린 공식문서를 보면 noinline을 다음과 같이 설명한다.
"인라인 함수에 전달된 모든 람다를 인라인으로 처리하지 않으려면 함수 매개 변수 중 일부에 noinline 키워드를 붙이면 된다."
위 코드를 보면 actionB는 functionB에서 사용된다. 우리는 actionB를 전달하고싶기 때문에 noinline 키워드를 붙여보자.
noinline
noinline을 붙이면 위와 같이 컴파일 에러가 사라지는 것을 확인할 수 있다.
전달받은 함수들 중 일부는 다른 함수로 넘겨줘야할 때와 같이, 모든 인자를 inline 처리하고 싶지 않을 때가 있을 것이다. 이럴 때 사용하는 키워드가 바로 noinline 이다. inline 에서 제외시킬 인자 앞에 noinline 키워드를 붙이면, 그 순간 이후로 해당 인자는 다른 함수로 전달할 수 있다.
그러면 모든 함수에 inline을 붙이면 좋은 것인가?
그것은 아니다.
일반 함수에서 inline
- 일반 함수 호출의 경우에는 이미 JVM에서 강력하게 인라이닝을 지원해준다.
- JVM은 코드 실행을 분석해서 가장 유리한 방향으로 인라이닝 해준다.
- 즉, 일반 함수에서 inline을 붙이면 코드 중복이 생기게 된다.
고차 함수에서 inline
- 많은 코드를 갖고 있는 고차 함수를 inline 처리하면 바이트코드의 양이 훨씬 많아지게 된다.
- 이 경우 성능이 오히려 악화될 수도 있다.
- 다른 글들을 보니 inline 처리는 1~3줄, 1~5줄 정도의 길이를 권장하고 있다고 하는데 출처를 알 수 없다.
- 공식문서에서도 큰 함수에는 inline을 사용하지 말라고만 한다.
테스트를 해보았다
위에 대한 해답을 찾기 위해 인텔리제이의 Profier를 통해 inline을 사용한 경우와 사용하지 않은 경우를 CPU와 메모리 사용량을 확인하며 테스트 했다.
공식문서에서 말하는 큰 함수는 로직이 복잡한 함수
작은 함수는 로직이 간단한 함수
위 가정을 토대로 테스트를 진행했다.
나는 간단한 로직일 수록 inline을 사용한 것이 더 효율적이고, 로직이 복잡할 수록 inline을 사용하지 않은 것이 더 효율적으로 나올 것을 예상했다.
그러나 결과는 매번 Inline을 사용했을 떄가 효율적인 것으로 나왔다.
내가 테스트를 잘 못했을 수도 있지만 나의 결론은 다음과 같다.
- 내가 테스트를 잘 못했을 수도 있다.
- 생각보다 로직이 복잡하다는 의미는 내가 한번도 보지 못한 코드일 수도 있겠다.
- 큰 함수란 기준은 사람마다 다르기 때문에, 개발한 사람이 큰 함수라고 하면 큰 함수일 수 있다.
- 잘은 모르지만, 그냥 본인만에 기준을 잡고 inline을 사용하면 되지 않을까?
- 나는 고차함수의 호출 횟수와 크기에 따라서 inline을 사용할 지 정할 것 같다.
탐험 일지
- inline을 사용한다고 무조건 성능이 좋아지지는 않을 수 있다.
- inline은 고차 함수와 함께 적절하게 사용하는 것이 좋다.
- 함수 전달이 아닌 경우라면 JVM이 인라이닝을 해줄테니 우리는 하지말자.
- 함수 전달 시에는 inline을 명시해 주는 게 좋다.
- 빈번하게 사용되거나, 호출 오버헤드를 줄이기 위해 사용하는 게 좋다.
- noinline은 파라미터로 제3의 함수에 전달할 때 사용한다.
휴.. 이제 inline을 다 탐험했다
는 뻥이고.. 공부하던 중 crossinline, reified, Inline properties 키워드를 발견해버렸다.. 이건 다음에 알아보도록 하자.
'Develop > Kotlin' 카테고리의 다른 글
[Android] 너는 왜 inline Composable이야? (1) | 2024.10.22 |
---|---|
[Android] 데이터 바인딩 프로퍼티 실종 사건 수사 일지 (37) | 2024.05.12 |
[Kotlin] 확장 함수 (0) | 2024.04.03 |
[Kotlin] Data class (0) | 2024.04.03 |
[Kotlin] enum와 sealed class (0) | 2024.02.26 |