Kotlin inline, noinline, crossinline

바보·2023년 12월 5일

Kotlin 구멍 메꾸기

목록 보기
2/2

문제점

fun String?.nonNull(block: String.() -> Unit) {
	this?.let(block)
}

위와 같이 람다식을 사용하면 오버헤드를 발생시킬 수 있다. 람다식은 컴파일 타임에 무명 객체를 생성하기 때문에 추가적인 객체 생성 비용이 든다.

이러한 문제점을 해결하고자 나온 것이 inline 함수이다. inline 키워드를 붙인 함수는 파라미터로 넘어 온 람다식의 본문이 함수 내부로 붙여넣기(인라이닝)된다. 객체를 생성하지 않고 함수 본문 자체를 인라이닝하기 때문에 오버헤드를 막을 수 있다.

실제 예제를 통해 살펴보기

위 글만 읽었을 때는 이해가 잘 안 되니 예제를 통해서 살펴보자.

일반 함수인 nonNull() 함수와 inline 함수인 nonNullInlined() 함수를 정의해주었다.

fun String?.nonNull(block: String.() -> Unit) {
    this?.let(block)
}

inline fun String?.nonNullInlined(block: String.() -> Unit) {
    this?.let(block)
}

fun main() {
    var message = ""

    message.nonNull {
        this + "Hello"
    }

    message.nonNullInlined {
        this + " World"
    }
}

이 코드의 main() 함수를 자바 코드로 디컴파일하면 다음과 같다.

public static final void main() {
	// 일반 함수는 Function 객체를 생성하여 람다를 실행함
	String message = "";
	nonNull(message, (Function1)null.INSTANCE);
    
    // inline 함수는 객체를 생성하지 않고 람다의 본문이 인라이닝됨
	int nonNullInlined = false;
	int var5 = false;
	(new StringBuilder()).append(message).append(" World").toString();
}

일반 함수로 정의된 nonNull() 함수는 람다식을 실행하기 위해서 Function 객체를 생성하는 반면, inline으로 정의된 nonNullInline() 함수는 객체를 생성하지 않고 람다의 본문이 인라이닝된 것을 볼 수 있다.

다양한 인라인 함수

inline

가장 기본적인 inline, 함수 앞에 inline 키워드만 붙여주면 된다.

inline fun doSomething(execution: () -> Unit) { ... }

noinline

인라이닝 하지 않을 람다식을 파라미터로 전달하고자 할 때, noinline을 사용하면 된다.

inline fun doSomething(noiline execution: () -> Unit) { ... }

crossinline

crossinline을 이해하려면 non-local return의 개념부터 알아야 한다.

💡 non-local return (비지역 반환)
non-local return은 해당 블럭의 스코프를 벗어나는 반환을 의미한다.
일반적으로 코틀린에서는 람다가 외부 함수를 종료할 수는 없다.

// 람다를 파라미터로 받는 일반 함수
fun <T, R> T.transform(blcok: T.() -> R) = this.block(this)

fun foo() {
	"Seungyeon".transform {
		// 오류, transform() 함수의 scope를 벗어나는 반환이기 때문에 불가능
		if (this.isBlank()) return
		this + "Baboo"
	}
}

하지만 인라이닝으로 선언된 함수는 다르다. 컴파일 타임에 자동으로 람다의 본문이 인라이닝되기 때문에 람다 내부에서도 foo() 함수의 반환이 가능하다.

inline fun <T, R> T.transform(blcok: T.() -> I): R = this.block(this)

fun foo() {
	"Seungyeon".transform {
		// 종료 가능!
		if (this.isBlank()) return 
		this + "Baboo"
	}
}

// 사실상 위 코드를 컴파일하면 아래와 동일하다
fun foo() {
	var message = "Seungyeon"
	if (message.isBlank()) return
	message += "Baboo"
}

정리하면! 기본적으로 inline으로 정의한 함수는 non-local return을 지원한다. crossinline은 이 non-local return을 방지하고자 할 때 사용하면 된다.

근데 IDE 차원에서 crossinline의 선언이 필요한 코드는 자동으로 오류를 띄워주기 때문에 크게 신경 쓸 필요는 없을 것 같다.

🤔 non-local return에 대해..

개인적으로는.. crossinline을 지정해 non-local return을 방지해주는게 맞다고 생각한다.

자신의 scope를 벗어난 반환이라는 것부터 가독성을 해친다고 생각된다. 또, 해당 언어의 문법이 제대로 숙지 되지 않은 상태라면 코드를 추적하는게 더더욱 어려워질 것이라고 예상된다. (람다의 return@label 문법처럼...)

fun foo(name: String) {
	if (name.isEmpty()) return
    name.transform {
    	// non-local return에 local return에.. 어질어질
    	if (name.isBlank()) return
        if (name.length != 3) return@transform
        "I'm $this."
    }
    ...
}

코틀린을 비롯한 많은 언어에서 non-local return을 지원하지 않는 이유가 괜히 있을까?

언제 인라이닝을 사용해야 하나

똑똑한 JVM

일반 함수를 호출할 때는 굳이 인라이닝을 표시할 필요는 없다. 똑똑한 JVM이 알아서 인라이닝이 필요한 코드에는 인라이닝을 지원해주기 때문이다.

하지만 JVM이 람다식을 파라미터로 받는 함수를 인라이닝해줄 정도로 똑똑하지는 못하다고 한다. 그렇기 때문에 람다를 파라미터로 받는 함수를 정의한다면, 직접 inline 함수를 정의해주어야 한다.

결론은 람다를 파라미터로 받을 때만 사용하자

물론 인라이닝이 모든 상황에서의 해법은 아니다. 함수의 내용이 엄청나게 길다면, 오히려 인라이닝하는 하는게 더 많은 리소스를 낭비할 수도 있다.

그래서 결론은 람다를 파라미터로 받을 때만 inline 함수로 정의하자~

정리

  • 람다를 파라미터로 받는 경우, 함수를 inline으로 정의하면 무명 객체를 생성하지 않아 오버헤드를 막을 수 있다.
  • 종류
    • inline: 인라이닝을 지원하고자 하는 함수에 사용
    • noinline: 인라이닝하지 않을 람다를 파라미터로 넘기고자 할 때 사용
    • crossinline: non-local return을 막고자 할 때 사용


profile
이전: https://sseung416-dev-note.vercel.app

0개의 댓글