코틀린에서는 고차함수를 사용할 수 있다. 함수(람다)를 함수의 호출 인자로 전달하거나, 반환값으로 활용할 수도 있다. 그런데 이렇게 람다를 사용하게 되면, 부가적인 메모리 할당으로 인해 메모리 효율이 안 좋아지고, 함수 호출로 인한 런타임 오버헤드가 발생하게 된다.
인라인 함수를 사용한다면 람다식을 사용했을 때 무의미한 객체 생성을 막을 수 있다.
fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}
이 코드를 컴파일하면 아래와 같은 java 파일이 된다. Functional Interface인 Function 객체를 파라미터로 받고 invoke 메서드를 실행한다.
public static final void doSomethingElse(Function0 lambda) {
System.out.println("Doing something else");
lambda.invoke();
}
위의 함수를 사용하기 위해서는 아래처럼 코드를 작성할 수 있다.
fun doSomething() {
println("Before lambda")
doSomethingElse {
println("Inside lambda")
}
println("After lambda")
}
그렇다면 이런 코틀린 코드는 컴파일되서 어떤 자바코드를 가지게 될까? 바로 아래와 같은 코드가 나오게 된다. doSomethingElse의 파라미터로 새로운 객체를 생성하여 넘겨준다는 것이다. 이 객체는 doSomething이라는 메서드를 호출할 때마다 새로이 만들어진다. 무의미하게 새로운 객체를 매번 생성하는 것이다.
public static final void doSomething() {
System.out.println("Before lambda");
doSomethingElse(new Function() {
public final void invoke() {
System.out.println("Inside lambda");
}
});
System.out.println("After lambda");
}
이러한 문제점을 해결해주는 것이 바로 인라인(inline) 함수다. 인라인 함수를 사용하게 되면 코드는 객체를 항상 새로 만드는것이 아니라 해당 함수의 내용을 호출한 함수에 넣는 방식으로 컴파일 코드를 작성하게 된다.
inline fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}
아래는 사용하는 곳을 컴파일한 코드이다. 무의미하게 Function 객체를 항상 만들어내는 것이 없어졌다.
public static final void doSomething() {
System.out.println("Before lambda");
System.out.println("Doing something else");
System.out.println("Inside lambda");
System.out.println("After lambda");
}
fun doSomething() {
val greetings = "Hello" // Local variable
doSomethingElse {
println("$greetings from lambda") // Variable capture
}
}
컴파일을 해보면 람다식에서 사용하는 지역 변수는 아래와 같이 Function 객체의 생성자의 변수로 들어가는 것을 확인할 수 있다.
public static final void doSomething() {
String greetings = "Hello";
doSomethingElse(new Function(greetings) {
public final void invoke() {
System.out.println(this.$greetings + " from lambda");
}
});
}
객체에 변수가 추가되었다. 즉, 객체의 메모리 사용량이 늘어났다는 것이다. 이 경우 인라인 함수를 사용하면 좀 더 나은 성능을 보장할 수 있을 것이라는 사실을 알 수 있다.
일단 기본적으로 JVM의 JIT 컴파일러에 의해서 일반 함수들은 inline 함수를 사용했을 때 더 좋다고 생각되어지면 JVM이 자동으로 만들어주고 있다. 그리고 inline 함수를 사용하면 좋지 않거나 사용이 불가능할 경우도 있다.
또, 많은 코드를 갖고 있는 람다를 inline 처리하면 바이트코드의 양이 훨씬 많아지게 된다. 이 경우 성능이 오히려 악화될 수도 있다. 따라서 inline 처리는 1~3줄 정도의 길이를 권장하고 있다.
전달받은 함수들 중 일부는 다른 함수로 넘겨줘야할 때와 같이, 모든 인자를 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 static final void firstMethod(int a, @NotNull Function0 func1, @NotNull Function0 func2) {
int $i$f$firstMethod = 0;
Intrinsics.checkNotNullParameter(func1, "func1");
Intrinsics.checkNotNullParameter(func2, "func2");
func1.invoke();
secondMethod(10, func2);
}
public static final int secondMethod(int a, @NotNull Function0 func) {
Intrinsics.checkNotNullParameter(func, "func");
func.invoke();
return 2 * a;
}
public static final void main() {
boolean var0 = true;
Function0 func2$iv = (Function0)null.INSTANCE;
int $i$f$firstMethod = false;
int var3 = false;
String var4 = "Just some dummy function";
System.out.println(var4);
secondMethod(10, func2$iv);
}
코드를 보면 func2() 를 제외한 나머지 코드들은 인라인 처리가 되었고, func2() 는 기존 방식대로 객체를 새로 생성하여 호출되는 것을 확인할 수 있다.
public 인라인 함수는 private 함수에 접근할 수 없다.
inline fun doSomething() {
doItPrivately() // Error
}
private fun doItPrivately() { }
https://velog.io/@haero_kim/Kotlin-Inline-Function-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0
https://sabarada.tistory.com/176