Inline 함수란? 근데 Higher-Order Functions이랑 Lambdas는?

권태용·2020년 10월 12일
0

Kotlin

목록 보기
2/6

사내 코드를 참조하다 inline 키워드를 찾았다 fun 앞에 명시해주었는데 이게 무엇을 의미하는지 찾아보았다.
처음엔 inline의 의미만 찾아보려했는데 고차함수 및 람다함수의 표현식도 알아보게 되었다.

Higher-Order Function이랑 Lambda는?

inline의 사용이유는 Higher-Order Functions이랑 Lambdas에 있기 때문에 Higher-Order Functions이랑 Lambdas를 먼저 알아보자

우선 Higher-Order Function(고차 함수)는 함수의 파라미터 또는 리턴 값을 다른 함수로 정의 한 것을 의미한다.
예시로 Collection에 fold 함수를 보면 파라미터로 함수를 전달 받고 accumulator란 값을 리턴한다.

fun <T, R> Collection<T>.fold(
    initial: R, 
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

Lambda란?

람다란 함수를 선언하지 않고 바로 표현식으로 전달하는 것을 의미한다. 때문에 sum라는 함수를 정의 할때 아래와 같이 한다면 sum은 함수 선언부분이 되고 {....} 절이 바로 전달 되어 sum의 매개변수와 리턴 타입이 추론을 통해 자동적으로 정의 된다.

val sum = { x: Int, y: Int -> x + y }

람다함수의 활용팁은 https://kotlinlang.org/docs/reference/lambdas.html#higher-order-functions 원문을 통해 확인하면 좋을것 같습니다. (함수호출후 람다식 표현, 사용하지 않는 변수 선언, 리시버가 있는 함수리터럴.. 등등등)

Closures(Higher-Order-function과 Lambda의 특징)

위에서 inline의 활용을 위한 Higher-Order-function과 Lambda를 간단하게 알아보았다.
하지만 inline을 효율성을 이해하기 위해선 Closures라는 개념이 필요하다. 클로저란 함수 밖에서 선언된 변수를 함수내에서 사용할 수 있다는 것이다.

fun main(args: Array<String>) {
    var ints = listOf(1, 2, 3)
    var sum = 0
    ints.filter { it > 0 }.forEach {
        sum += it
    }
    print(sum)
}

위 함수를 보면 sum이란 main함수의 변수를 filter에 정의된 람다식 함수에서 접근 할 수 있다. 너무 당연하게 생각해서 이 특징을 놓치고 있었다.

코틀린에서 함수란? 그리고 Closures가 가능한 이유는?

코틀린에서 함수는 컴파일시에 FunctionXX(XX는 숫자)이라는 이름의 객체로 변환된다. 그리고 객체이기 때문에 각 함수의 변수를 공유할 수 있다.

예시 코드를 통해 이해 해보자. 코드의 내용은 add라는 함수가 f1이라는 함수를 리턴하고 f1을 호출하여 result에 담는다. 이때 add에서 선언된 x라는 변수를 f1이 사용해서 x와 y값을 더 한다.

fun add(x: Int): (Int) -> Int {
    return fun(y: Int): Int {
        return x + y
    }
}

fun main(args: Array<String>) {

    val f1 = add(1)
    val result = f1(2)

    print(result)
}

위 코드의 디컴파일 결과는 아래와 같다

디컴파일 결과를 확인해 보면 add는 Function1 객체를 리턴하고 add가 리턴하는 함수는 Function1에 함수가 되었다. 때문에 add 함수 호출은 Function1객체를 생성하고 해당 객체에서 x와 y를 더하게 된다.

inline을 사용하는 이유

Using higher-order functions imposes certain runtime penalties: each function is an object, and it captures a closure, i.e. those variables that are accessed in the body of the function. Memory allocations (both for function objects and classes) and virtual calls introduce runtime overhead.

-> higher-order 함수는 런타임중 페널티를 강요한다. : 각 함수는 모두 object이고 closure를 캡쳐한다? 즉 이러한 변수는 함수에서 접근 할수 있는 변수들이다. 메모리 할당과 가상변수는 런타임시 오버헤드를 유발한다.

위에서 살펴 봤드시 코틀린에서 함수는 객체로 변환되고 이는 오버헤드를 유발한다. 이를 inline을 통해 해결 할 수 있다. inline 함수의 경우 함수호출이 아닌 코드 복사가 이루어지기 때문이다.

위에 예제코드의 경우 inline함수는 리턴값이 함수가 될 수 없다고 한다.. 해서 아래와 같이 코드를 수정하였다.

inline fun add(x: Int, y: (Int) -> Int): Int {
    return y.invoke(x)
}

fun main(args: Array<String>) {

    val result = add(1) {
        it + 1
    }

    print(result)
}

디컴파일 결과는 아래와 같다.

main 함수의 결과를 보면 add함수 선언의 호출이 아니라 result에 수식으로 표현되어있다. 코드가 복사 된 것이다.

무조건 inline?

위 결과를 보면 인라인 함수를 통해 오버헤드를 감소 시킬 수는 있다. 하지만 코드가 위와 같이 짧을때는 효과가 있을 수도 있다. 하지만 함수의 내용이 길어진다면 전체 bytecode양이 커지기 때문에 최적화 된다고 볼 수는 없을것 같다.

참조 내용
https://m.blog.naver.com/PostView.nhn?blogId=yuyyulee&logNo=221389623237&proxyReferer=https:%2F%2Fwww.google.com%2F - Inline 함수 디컴파일 결과 확인

https://ijeee.tistory.com/22 - Intelij에서 디컴파일 결과 확인하기
https://codechacha.com/ko/kotlin-inline-functions/

profile
개발일기장

0개의 댓글