Kotlin

inline 함수

kakaroo 2022. 4. 1. 10:08
반응형

인라인(inline) 키워드는 자바에서는 제공하지 않는 코틀린만의 키워드입니다. 이러한 인라인 키워드를 이용하여 함수를 만들고 이를 잘 활용한다면 다양한 이득을 얻을 수 있는 경우가 있습니다. 하나씩 알아보도록 하겠습니다.

 

A 함수에서 B 함수를 호출하면 B 함수로 진입하게 됩니다. 이때, A 함수에서 B 함수로 넘어가면서 발생하는 오버헤드가 있습니다. 이 과정은 큰 힘을 들이지는 않지만, 경우에 따라서는 속도에 유의미한 영향을 끼치기도 합니다.

 

아래 함수를 살펴보겠습니다.

main 함수 -> sum 함수 -> main 함수 -> sum 함수 -> ...  이런식으로 오버헤드가 발생합니다.

 

함수 선언 앞에 inline 키워드를 붙이면 컴파일 될 때 해당 함수 코드가 호출되는 곳에 복사가 됩니다.

for 문으로 반복되기 때문에 중복된 코드로 인해 프로그램 사이즈가 그만큼 늘어나는 단점도 있습니다.

인라인 키워드 추가

 

인라인 예제 - 람다를 파라미터로 받는 고차함수
인라인 예제 - 일반 함수

 

 


 

함수를 inline으로 선언해야 하는 경우

 

 inline 키워드를 사용한다 해도 람다를 인자로 받는 함수 성능이 좋아질 가능성이 높기 때문에, 그 외 다른 코드의 경우에는 주의 깊게 성능을 측정하고 분석해야 합니다.

 

람다를 파라미터로 받는 함수의 경우

람다를 파라미터로 받는 함수의 경우는 인라이닝을 하는게 훨씬 유리합니다.

 

인라인 함수를 사용하면 람다식을 사용했을 때 무의미하게 객체가 생성되는 것을 막을 수 있습니다. 이를 확인하기 위해서 우선 코틀린의 람다식이 컴파일될 때 어떻게 변하는지 확인해보겠습니다.

fun nonInlined(block: () -> Unit) {
    block()
}

fun doSomething() {
    nonInlined { println("do something") }
}

nonInlined라는 함수는 고차 함수로 함수 타입을 인자로 받고 있습니다. 그리고 doSomething()은 noInlined 함수를 호출하는 함수입니다. 이러한 코드를 자바로 표현한다면 아래와 같습니다.

public void nonInlined(Function0 block) {
    block.invoke();
}

public void doSomething() {
    noInlined(System.out.println("do something");
}

이렇게 표현되는 코드는 실제로 컴파일하면 아래와 같이 변환됩니다. 이 코드에서의 문제점은 nonInlined의 파라미터로 새로운 객체를 생성하여 넘겨준다는 것입니다. 이 객체는 doSomething 함수를 호출할 때마다 새로 만들어집니다.

즉, 이와 같이 사용하면 무의미하게 생성되는 객체로 인해 낭비가 생기는 것입니다.

public static final void doSomething() {
    nonInlined(new Function() {
        public final void invoke() {
            System.out.println("do something");
        }
    });
}

 

람다식이 로컬 변수를 사용하는 경우 객체에 변수가 추가되어 메모리 사용량까지 늘어나게 됩니다.

fun nonInlined(block: () -> Unit) {
    block()
}

fun doSomething() {
    val hello = "Hello"	//Local variable
    nonInlined { println("$hello - do something") }	// Variable capture
}

public static final void doSomething() {
    String hello = "Hello";
    nonInlined(new Function(hello) {   //람다식에서 사용하는 지역변수가 Function 객체의 생성자의 변수로 들어감
            public final void invoke() {
               System.out.println(this.hello + " - do something");
           }
    });
}

 

이러한 문제점을 해결하기 위해서 인라인을 사용하는 것입니다. 인라인을 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해줍니다. 

즉, 객체가 항상 새로 생성되는 것이 아니라 해당 함수의 내용을 호출한 함수에 넣는방식으로 컴파일 코드를 작성하게 됩니다. 아래의 예제를 통해 알아보겠습니다.

인라인 키워드 사용

inline fun inlined(block: () -> Unit) {
    block()
}

fun doSomething() {
    inlined { println("do something") }
}

단지 inline 키워드를 함수에 추가하였습니다. 이를 컴파일한 코드를 보면 확인할 수 있지만 위와 같이 불필요한 객체를 생성하지 않고 내부에서 사용되는 함수가 호출하는 함수(doSomething)의 내부에 삽입됩니다.

public static final void doSomething() {
    System.out.println("do something");
}

noninline

둘 이상의 람다를 인자로 받는 함수에서 일부 람다만 인라이닝하고 싶을 때도 있을 수 있습니다. 예를 들면 어떤 람다에 너무 많은 코드가 들어가거나 어떤 람다에 인라이닝을 하면 안되는 코드가 들어갈 가능성이 있을 때 입니다. 이런 식으로 인라이닝하면 안 되는 파라미터를 받는다면 noinline 변경자를 파라미터 이름 앞에 붙여서 인라이닝을 금지할 수 있습니다.

inline fun sample(inlined: () -> Unit, noinline noInlined: () -> Unit) {
    
}

 

 


 

reified 키워드

범용성 좋게 함수를 만들기 위해서 generics class Type을 이용할 수 있습니다. 

fun <T> doSomething(someValue: T)

하지만, class Type T 객체는 타입에 대한 정보를 Compile하면서 타입 정보를 제거하여 Runtime에는 T가 어떤 타입인지 모릅니다. 그냥 T로 정해진 객체가 존재할 뿐이죠. 그래서 아래와 같이 실행하면 에러가 발생하게 됩니다. 왜냐하면 타입을 알 수가 없기 때문입니다. 따라서 Class<T>를 함께 넘겨 type을 확인하고 casting 하는 과정을 거치곤합니다.

fun <T> doSomething(someValue: T, Class<T> type) { // runtime에서도 타입을 알 수 있게 Class<T> 넘김
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // Error
}

이러한 문제점에 때문에 reified 키워드를 사용하면 됩니다. 인라인(inline) 함수와 reified 키워드를 함께 사용하면 T type에 대해서 런타임에 접근할 수 있게 해줍니다. 따라서 타입을 유지하기 위해서 Class<T>와 같은 추가 파라미터를 넘길 필요가 없어집니다.

 

inline fun <reified T> doSomething(someValue: T) {
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // OK
}

 

요약 : Reified 키워드는 Generics로 inline function에서 사용되며, Runtime에 타입 정보를 알고 싶을 때 사용합니다.

Generics 코드를 컴파일할 때 컴파일러는 T가 어떤 타입인지 알고 있습니다. 하지만 Compile하면서 타입 정보를 제거하여 Runtime에는 T가 어떤 타입인지 모릅니다. 그냥 T로 정해진 객체가 존재할 뿐이죠.

Reified 키워드를 사용하면 Generics function에서 Runtime에 타입 정보를 알 수 있습니다. 하지만 inline function과 함께 사용할 때만 가능합니다.

 

아래 예를 하나 더 들어보겠습니다.

fun printString(value: String) {
    when (value::class) {
        String::class -> {
            println("String : $value")
        }
    }
}

printString("print string function")

코드를 실행해보면 의도한 문자열이 출력됩니다.

String : print string function

위의 코드를 Generics로 만들고 싶습니다.

다음과 같이 타입에 따라 출력하는 내용을 다르게 만들고 싶습니다. 하지만 아래 코드는 컴파일 에러가 발생합니다. Runtime에 타입 정보가 지워지기 때문입니다.

fun <T> printGenerics(value: T) {
    when (value::class) {  // compile error!
        String::class.java -> {
            println("String : $value")
        }
        Int::class.java -> {
            println("Integer : $value")
        }
    }
}

이럴 때 타입 정보가 있는 객체를 인자로 전달하면 문제를 해결할 수 있습니다.

아래와 같이 Class<T>로 인자를 전달하였고 이것으로 타입을 알 수 있습니다.

fun <T> printGenerics(value: T, classType: Class<T>) {
    when (classType) {
        String::class.java -> {
            println("String : $value")
        }
        Int::class.java -> {
            println("Int : $value")
        }
    }
}

printGenerics("print generics function", String::class.java)
printGenerics(1000, Int::class.java)

위의 코드를 실행해보면 다음과 같이 출력됩니다.

String : print generics function
Integer : 1000

지금까지 Reified 키워드 없이 Runtime에 Generics의 타입 정보를 알 수 있는 예제를 작성해보았습니다. 물론 위의 코드를 사용한다고 해서 문제될 것은 없지만, 타입 정보를 얻기 위해 인자 1개를 추가로 넘겨야하는 것이 깔끔해보이진 않습니다.

 

reified 키워드를 inline과 같이 사용하면 깔끔하게 해결 할 수 있습니다.

 

inline fun <reified T> printGenerics(value: T) {
    when (T::class) {
        String::class -> {
            println("String : $value")
        }
        Int::class -> {
            println("Integer : $value")
        }
    }
}

 

그렇다면 항상 inline을 사용하는게 좋은거 아닐까?

 

inline 함수를 만들 때 코드 크기가 큰 함수의 경우는 모든 호출 지점에 바이트코드가 복사되기 때문에 오히려 더 성능을 악화시킬 수 있기 때문에 가급적이면 코드 크기가 작은 부분에만 inline 함수를 사용하면 좋을 것 같습니다!

실제로, 코틀린 라이브러리가 제공하는 inline 함수를 보면 모두 다 크기가 아주 작다는 사실을 알 수가 있습니다.

 

그리고 public inline 함수는 private 함수를 호출할 수가 없는 제약이 있습니다.

 

반응형

'Kotlin' 카테고리의 다른 글

코루틴(Coroutine)  (0) 2022.04.03
BlogViewer (feat. JSoup Crawling)  (1) 2022.04.03
enum class, sealed class  (0) 2022.03.31
sequence  (0) 2022.03.27
collection  (0) 2022.03.27