ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin] inline function 정리 1
    Kotlin/Function 2021. 12. 15. 11:11

    inline function의 개념을 이해하기 위해서는 람다식, 고차함수, 함수타입 변수에 함수객체를 생성하는 법을 이해하고 있어야 한다. 설명하는 내용들은 아래의 포스팅 시리즈에 있다.

    [Kotlin/Function] - [Kotlin] 코틀린 함수와 함수형 프로그래밍 1

     

    inline function의 선지식 조건이 많아 세편정도의 포스팅으로 기록할 것 같다.

     

    람다식, 함수타입 변수에 대입된 함수, 고차함수에 전달될 인자의 함수

    위의 세가지에 해당하는 함수는 실제로 Function0~ Function22 인터페이스를 구현한 객체로 만들어 진다.

    (함수의 파라메터 개수에 따라 Function0~ Function22)

    또한 해당 함수를 호출하기 위해서는 객체의 invoke함수를 사용하는데, invoke함수의 특성상 invoke이름을 생략하고 호출할 수있기 때문에 마치 함수가 호출된 것처럼 느낄 수 있었다.

     

    1. 람다식, 함수타입 변수에 대입된 함수

    fun firstOrder(): Unit {
        println("일차 함수 코드 시작")
        println("일차 함수 코드 끝")
    }
    
    fun main() {
    
        val test: Function0<Unit> = ::firstOrder //함수 참조
        //동일 val test: () -> Unit = ::firstOrder
        test.invoke() //동일 test()
        
        println()
        
        val test2: Function0<Unit> = {
            println("람다식으로 만든 일차 함수 코드 시작")
            println("람다식으로 만든 일차 함수 코드 끝")
        } //람다식
        test2.invoke() //동일 test2()
    
    }

     

    코틀린 바이트 코드 

    함수를 호출하는 것이 아닌 함수 자체를 담는 코드들은 실제로 Function0인터페이스를 구현한 객체를 통해 함수가 담긴다

    (위의 함수는 파라메터가 없으므로 Function0이고, 리턴타입이 Unit이므로 Function0<Unit> 인터페이스 구현 객체로 함수가 담긴다)

    함수의 내용은 invoke메서드에 구현되어 있어, invoke메서드를 호출하여 실제 함수를 호출하였다.


    2. 고차함수의 인자 함수들

    fun firstOrder(): Unit {
        println("일차 함수 코드 시작")
        println("일차 함수 코드 끝")
    }
    
    fun highOrder(para: () -> Unit) {
        
    }
    
    fun main() {
        val first = ::firstOrder
        highOrder(first)
    }

     

    이렇게 함수의 호출이 아닌 함수 자체 (1. 람다식, 함수타입 변수에 대입된 함수 2. 고차함수의 인자 함수)는 실제로 Function인터페이스를 구현한 객체로 관리된다.

     

    이제 inline 함수의 동작을 살펴보자.


    caller 함수가 inline function을 호출한 부분은, 컴파일 과정에서 caller 함수 내부에 inline 함수 내용 전체가 코드로 삽입된다.

     

    쉬운 이해를 위해 간단한 함수부터 살펴보자.

     

    • inline 키워드가 붙지 않은 일반 firstOrder 함수
    fun firstOrder(): Unit {
        println("일차 함수 코드 시작")
        println("일차 함수 코드 끝")
    }
    
    fun main() {
        firstOrder()
    }

     

    main에서 함수를 단순히 호출하는 식이다.


    • inline 키워드가 붙은 firstOrder 함수
    inline fun firstOrder(): Unit {
        println("일차 함수 코드 시작")
        println("일차 함수 코드 끝")
    }
    
    fun main() {
        firstOrder()
    }

     

    firstOrder함수를 호출하지 않고, firstOrder 함수 코드 전체가 main함수의 내부에 추가되었다.

    이렇듯이 프로그램상 caller에서 inline function을 call한 모든 코드들은 컴파일과정에서 함수 호출대신 함수 코드 내용이 caller 함수 내부에 삽입된다.


    실제로 inline 함수는 고차함수 때문에 사용한다.

     

    inline 키워드를 고차함수에 사용하면 고차함수의 Runtime 패널티를 줄일 수 있다.

     

    • 먼저 inline 키워드를 사용하지 않은 highOrder 고차함수를 보자
    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    fun highOrder(para: () -> Unit): Unit {
        println("고차함수 호출")
        para()
    }
    
    fun main() {
        highOrder(::firstOrder)
    }

     

    고차함수는 inline 키워드를 사용하지 않았으므로 main에서 호출될 때 고차 함수가 직접 호출된다.

    고차함수가 호출되기전에 인자에 해당하는 firstOrder 함수내용이 인자로 전달되기 위해 Function인터페이스 구현 객체가 하나 생성되면서 이 객체가 전달된다.

    고차함수가 호출되고 내부에서 구현 객체의 invoke를 통해 일차함수 내용의 복사본에 해당하는 invoke 함수가 호출된다.


    • inline 키워드를 사용한 highOrder 고차함수를 보자
    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    inline fun highOrder(para: () -> Unit): Unit {
        println("고차함수 호출")
        para()
    }
    
    fun main() {
        highOrder(::firstOrder)
    }

    고차함수가 호출되지 않고 고차함수의 코드들이 main에 삽입되었다.

    이때 일차함수에 해당하는 Function인터페이스 구현 객체가 생성되지 않고, 실제 일차함수가 존재하는 곳을 참고하여 함수자체를 직접 호출한다.

     

    차이를 정리해보자.

     

    inline 키워드를 사용하지 않았을 때는 실제 함수의 내용을 invoke함수에 카피하여 Function인터페이스를 구현한 객체를 만들어 객체변수.invoke() = para.invoke() = para()로 호출하였지만

    inline 키워드를 사용하였을 때는 고차함수의 인자에 해당하는 객체를 생성하지 않고 실제 함수에 해당하는 firstOrder()호출로 실제 함수를 호출하는 코드가 들어간다.

    ->->

    inline 함수를 사용하면 컴파일시 inline 함수가 호출되는 부분에 호출대신 함수 코드 전체가 추가되므로 바이트 코드의 양이 늘어나게 되는 단점을 가지면서, 함수에 해당하는 객체를 만들지 않아 heap 메모리를 아낄 수 있다는 장점이 공존한다.


    이러한 inline 동작은 inline 함수가 호출시에만 caller에 inline 함수 호출대신 inline 함수 전체 코드를 삽입하는 것이다. Function인터페이스를 구현한 객체를 생성하는 코드와는 무관하다. (람다식을 통해 함수를 생성하는 것, 람다식을 통해 함수를 생성해 고차함수에 전달하는 것, 리플렉션을 통해 함수를 참조하는 것)

    inline fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    inline fun highOrder(para: () -> Unit): Unit {
        println("고차함수 호출")
        para()
    }
    
    fun main() {
        val test: () -> Unit = ::firstOrder
        val test2: (() -> Unit) -> Unit = ::highOrder
    }

     

    inline 동작은 caller와 callee의 관계에만 연관있다.

    여기까지는 이해하기가 나름 쉬웠다.


    inline 고차함수의 인자에 해당하는 함수들의 동작을 noinline을 통해 따로 관리할 수 있다.

     

    inline 선언된 고차함수의 인자에 해당하는 함수는 모두 inline이 적용된다.

    (컴파일시 인자에 해당하는 함수 객체를 생성하는 코드 대신, 실제함수를 호출하는 코드가 들어감)

     

    inline함수의 모든 인자들은 inline으로 설정되기 때문에, inline이 아닌 동작을 할 수 있도록

    인자의 함수들을 inline을 적용을 막기 위해 noinline 키워드를 사용할 수 있다.

    highOrder가 inline이 아니면 인자 함수 모두 noinline이므로 noinline키워드를 사용하는 것이 의미가 없어, 개발자의 혼동을 막기 위해 noinline 키워드 사용을 막은 것 같다.

     

    또한 인자가 함수인 것에만 noinline 키워드를 사용가능하다.(함수 객체를 생성하냐 마냐에 대한 동작이므로)

    noinline para : 클래스 //x
    noinline para2 : String //x

     

    inline 고차함수의 noinline, inline동작을 보기 위해 고차함수의 인자를 두개로 변경하고 처음부터 다시 확인해봤다.

     

    • 일반 고차함수
    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    fun highOrder( para1: () -> Unit,  para2: () -> Unit): Unit {
        println("고차함수 호출")
        para1()
        para2()
    }
    
    fun main() {
        highOrder(::firstOrder, ::firstOrder)
    }

    고차함수에 inline이 적용되지 않아 고차함수가 직접 호출된다. 고차함수가 noinline이니 두 인자 함수 모두 noinline이다.

    호출 전 고차함수의 인자에 해당하는 두 함수가 객체로 만들어지고, 고차함수의 내부에서 객체변수(para1,para2)의 invoke(실제함수의 카피본 함수)가 호출된다.


    • inline 고차함수
    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    inline fun highOrder( para1: () -> Unit, para2: () -> Unit): Unit {
        println("고차함수 호출")
        para1()
        para2()
    }
    
    fun main() {
        highOrder(::firstOrder, ::firstOrder)
    }

    고차함수가 inline이여서 고차함수 호출(highOrder(...))대신 코드가 삽입되었다.

    이때 두 파라메터 함수 모두 noinline을 설정하지 않아 inline이 적용된다.

    두 함수모두 inline 설정되어 객체로 생성되지 않고 -> 실제 함수가 존재하는 위치에서 참조하여 직접 호출(firstOrder())되었다.


    • inline 고차함수 (+ para1만 noinline)
    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    inline fun highOrder(noinline para1: () -> Unit, para2: () -> Unit): Unit {
        println("고차함수 호출")
        para1()
        para2()
    }
    
    fun main() {
        highOrder(::firstOrder, ::firstOrder)
    }

    고차함수가 inline이여서 고차함수 호출대신 코드가 삽입되었다.

    noinline으로 선언된 para1만 함수 객체가 생성됨 -> 함수호출시 이 객체 para1.invoke()를 통해 호출됨

    para2는 inline이라 함수 객체가 만들어지지 않음 -> 실제 함수가 존재하는 위치를 통해 함수가 호출된다.


    • inline 고차함수 ( + 모든 인자 noinline )
    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    inline fun highOrder(noinline para1: () -> Unit, noinline para2: () -> Unit): Unit {
        println("고차함수 호출")
        para1()
        para2()
    }
    
    fun main() {
        highOrder(::firstOrder, ::firstOrder)
    }

     

    고차함수가 inline이여서 고차함수 호출대신 코드가 삽입되었다.

    두 파라메터 모두 noinline이라 두 함수 모두 객체로 생성된다.

    또한 두 객체 모두 객체의 invoke함수로 함수가 호출된다.

     

    일반 고차함수, inline 고차함수(+모든 인자 noinline) 의 코드차이를 살펴본 결과

    고차함수의 코드가 삽입되었냐, 고차함수가 호출되었냐의 차이만 있지 인자의 함수에 해당하는 객체 두개는 모두 생성되는 것을 볼 수 있다. (메모리 측면에서 둘다 동일하다)


    inline이 적용된 고차 함수의 인자에 해당하는 함수가 inline이라면 해당 인자는 다른 함수에 전달될 수 없다.

    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    fun anotherHighOrder(para: () -> Unit) {}
    
    inline fun highOrder(noinline para1: () -> Unit, para2: () -> Unit): Unit {
        println("고차함수 호출")
        anotherHighOrder(para1)
        //anotherHighOrder(para2) //컴파일 에러
    }
    
    fun main() {
        highOrder(::firstOrder, ::firstOrder)
    }

    inline인 para2는 다른 함수의 인자로 전달될 수 없다.

     

    조금 고민하였지만 단순하였다.

    함수의 인자로 전달시 항상 Function구현 객체를 전달하기 때문이다.

    inline인 para2는 Function구현 객체가 만들어지지 않기 때문에 다른 고차함수의 인자에 넣을 수 있는 것이 없는 것이다.

     

    nonline인 para1은 Function구현 객체가 생성되어 anotherHighOrder 고차함수의 인자에 객체를 전달할 수 있다.

    inline인 para2는Function구현 객체 자체를 생성하지 않아 anotherHighOrder 고차함수의 인자에 객체를 전달할 수 없다.

     

    둘다 noinline으로 바꾼코드이다. 위의 내용을 상기하면서 다시보자.

    inline으로 설정된 것은 Funtion구현 객체가 만들어지지 않아 다른 함수의 인자에 전달자체가 될 수 없다.

    현재는 둘다 noinline이라 Function구현 객체가 모두 만들어져서 anotherHighOrder의 인자에 전달이 가능하다.


    다른 함수의 인자에 넣어주지 못할뿐, 자체 호출은 둘다 가능하다.

    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    fun anotherHighOrder(para: () -> Unit) {}
    
    inline fun highOrder(noinline para1: () -> Unit, para2: () -> Unit): Unit {
        println("고차함수 호출")
        para1()
        para2()
    }
    
    fun main() {
        highOrder(::firstOrder, ::firstOrder)
    }

    noinline은 para1.invoke()로 , inline은 실제 함수가 호출된다.


    기초적인 내용은 대강 살펴본 것 같다... 조금 더 복잡하게 봐보자.

    fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    //inline
    inline fun highOrder(para: () -> Unit): Unit {
        para()
    }
    
    fun main() {
        highOrder(::firstOrder)
    }

     

    highOrder(...)호출 대신 코트들이 삽입된다. para가 inline이라서 Function구현 객체 생성대신 firstOrder()함수가 호출된다.


    firstOrder또한 inline인 경우 firstOrder가 호출되는 대신 코드들이 삽입된다.

    //inline 
    inline fun firstOrder(): Unit {
        println("일차함수 호출")
    }
    
    //inline
    inline fun highOrder(para: () -> Unit): Unit {
        para()
    }
    
    fun main() {
        highOrder(::firstOrder)
    }

    firstOrder()호출대신 코드들이 삽입됨

     

    댓글

Designed by Tistory.