ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin Inline modifier의 장점
    Java 2022. 1. 2. 21:00
    반응형

    inline modifier가 무엇인지 먼저 알아보고, 어떤 장점이 있는지 기술합니다. [1]의 내용을 중심으로 정리하였습니다.

     

    Inline Modifier란?

    코틀린의 stdlib을 살펴보면, 거의 모든 higher-order functions이 inline modifier를 달고 있는 것을 볼 수 있습니다. 예로, stdlib의 repeat function은 아래와 같습니다:

     

    inline fun repeat(times: Int, action: (Int) -> Unit) {
    	for (index in 0 until times) {
        	action(index)
        }
    }

     

    inline이 컴파일될 때에 하는 일은, 해당 함수를 사용하는 모든 곳을 함수의 body로 변경하도록 합니다. 또한, repeat 내부에서 함수 아규먼트를 호출하는 모든 곳을 아규먼트 함수(위에서 action)의 body로 변경합니다.

     

    그렇기에 아래와 같은 repeat 코드는:

     

    repeat(10) {
    	print(it)
    }

     

    컴파일 시에 아래와 같이 대체됩니다:

    for (index in 0 until 10) {
    	print(index)
    }

     

    이것은 보통 함수가 어떻게 실행되는지와 비교해보면 큰 차이를 보입니다. 일반적인 함수의 실행은, 해당 함수의 body로 점프해서 모든 statements를 invoke하고 함수를 호출했던 곳으로 돌아옵니다.

     

    Inline 시의 장점

    inline을 통해 replacing하는 것에는 아래와 같은 3가지 장점이 있습니다:

     

    1. 타입 아규먼트가 reified(의미상으로는 '구체화되다')될 수 있음
    2. functional 파라미터들을 가진 함수는 inline 시에 더욱 빠름
    3. Non-local 리턴이 허용됨

     

    타입 아규먼트가 refied될 수 있음

    Java의 이전 버젼에는 generics이 존재하지 않았습니다. Java 언어에 제네릭이 도입된 시점은 2004년의  J2SE 5.0에서입니다. 그러나 JVM 바이트코드에는 여전히 제네릭이 존재하지 않습니다. 그러므로 컴파일 동안에, 제네릭 타입은 지워집니다. 예로,  List<Int>는 List로 컴파일 됩니다. 이러한 이유로 인해 객체가 List<Int>인지 체크할 수 없습니다. 오직 객체가 List라는 점만 체크할 수 있습니다:

     

    any is List<Int> // Error
    any is List<*>   // OK

    Image from Author

     

    fun <T> printTypeName() {
    	print(T::class.simpleName) // Error
    }

     

    Inline을 사용해서 이러한 제약을 극복할 수 있습니다. inline이 추가된 함수 호출은 호출 부분이 함수의 body로 대체되기에, reified modifier를 사용하면 타입 파라미터를 사용하는 부분 역시도 타입 아규먼트로 대체되게 됩니다:

     

    inline fun <reified T> printTypeName() {
    	print(T::class.simpleName)
    }

     

    컴파일 시에, printTypeName의 body가 호출부를 대체하고 타입 아규먼트가 타입 파라미터를 대체합니다:

     

    // Usage
    printTypeName<Int>()
    printTypeName<Char>()
    
    // During compilation
    print(Int::class.simpleName)
    print(Char::class.simpleName)

     

     

    reified는 유용한 modifier로, stdlib의 filterIsInstance와 같은 곳에서 특정 타입의 요소만 걸러내기 위해 사용됩니다:

     

    class Worker
    class Manager
    
    val employees: List<Any> =
    	listOf(Worker(), Manager(), Worker())
        
    val workers: List<Worker> =
        employees.filterIsInstance<Worker>()

     

    functional 파라미터들을 가진 함수는 inline 시에 더욱 빠름

    정확히 말하면,  inlined 되었을 때 모든 함수는 조금 빠릅니다. 실행 시에 점프를 할 필요가 없고 back-stack을 추적할 필요가 없습니다. 이러한 이유로 stdlib의 함수는 대부분 inline되어 있습니다. 

     

    inline fun print(message: Any?) {
        System.out.print(message)
    }

     

    이러한 차이는 함수가 어떠한 functional 파라미터를 가지지 않을 때에는 그다지 큰 이점이 없습니다. 그렇기에 IntelliJ는 아래와 같은 워닝을 보여주는데요:

    Image from Author

     

    그 이유를 알기 위해서는, 객체로의 함수를 수행할 때 어떤 문제가 발생하는지를 이해할 필요가 있습니다. function literals를 사용해 생성되는 이러한 종류의 객체들은 어떤 이유로는 보관되어야 합니다.

     

    Kotlin/JS에서는 JavaScript이 함수를 first-class로 다루기에 단순합니다. 그러므로 Kotlin/JS에서, 이러한 객체는 함수거아 함수 레퍼런스입니다. Kotlin/JVM에서, 몇몇의 객체는 JVM 익명 클래스나 일반 클래스를 사용해 생성되어야 합니다. 그러므로 아래와 같은 람다 표현식은:

     

    val lambda: () -> Unit = {
        // code
    }

     

    클래스로 컴파일 되거나, JVM 익명 클래스로 컴파일 됩니다:

     

    // Anonymous
    Function0<Unit> lambda = new Function0<Unit>() {
    	public Unit invoke() {
        	// code
        }
    };
    
    public class Test$lambda implements Function0<Unit> {
    	public Unit invoke() {
            // code
    	}        
    };
    
    // Usage
    Function0 lambda = new Test$lambda()

     

    위 2가지 방법 간의 큰 차이는 없습니다. 

     

    주목할 부분은 function type이 Function0 type으로 변환되는 부분입니다. JVM 상에서 아규먼트를 가지지 않는  function은 이러한 function type으로 컴파일 됩니다. 다른 function type들은 아래와 같이 변환됩니다:

     

    •  () -> Unit은 Function0<Unit>으로
    • () -> Int는 Funtion0<Int)로
    • (Int) -> Int는 Function1<Int, Int>로
    • (Int, Int) -> Int는 Function2<Int, Int, Int)로

    위와 같은 인터페이스들은 kotlin 컴파일러에 의해 생성됩니다. 그러한 인터페이스는 온디맨드로 생성되기 때문에 사용자는 명시적으로 그러한 인터페이스를 사용할 수 없습니다. 대신 사용자는 funtion types을 사용할 수 있습니다:

     

    class OnClickListener: ()->Unit {
        override fun invoke() {
            \\ ...
        }
    }

     

    function의 body를 object로 래핑하는 것은 코드를 느리게 만드는데요. 그렇기에 아래의 2가지 코드 중에서 첫 번째가 더 빠릅니다:

     

    inline fun repeat(times: Int, action: (Int) -> Unit) {
        for (index in 0 until times) {
            action(index)
        }
    }

     

    fun repeatNoinline(times: Int, action: (Int) -> Unit) {
        for (index in 0 until times) {
            action(index)
        }
    }

     

     

    Non-local 리턴이 허용됨

    위에서 정의한 repeatNoinline을 사용하면 리턴이 불가능합니다:

     

    fun main() {
        repeatNoinline(10) {
            print(it)
            return // ERROR: Not allowed
        }
    }

     

    그 이유는 function literals이 컴파일된 결과 때문인데요. 코드가 다른 클래스 안에 위치할 때 main으로부터 return할 수 없습니다. function literal이 inline될 때에는 그러한 제약이 없습니다:

     

    fun main() {
        repeat(10) {
            print(it)
            return // OK
        }
    }

    코드가 어쨋든 main function 안에 위치하게 됩니다. 

     

     

     

     Reference

    [1] Effective Kotlin

    반응형
Kaden Sungbin Cho