인라인 함수

어떤 함수를 inline으로 선언하면 그 함수의 본문은 inline된다. 인라인 함수는 함수를 호출하는 바이트코드 대신에 함수 본문을 변역한 바이트코드를 컴파일한다는 의미를 갖는다.

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
  lock.lock()
  try {
    return action()
  } finally {
    lock.unlock()
  }
}

val l = Lock()
synchronized(l) {
  // ...
}

synchronized(l) { /* ... */ }을 사용하면 컴파일러는 이 인라인 함수를 아래와 같이 컴파일한다.

l.lock()
try {
  /* ... */
} finally {
  l.unlock()
}

인라인 함수 뿐만 아니라 람다의 본문도 함께 인라이닝된다. 그러나 람다가 아닌 함수 타입의 변수를 넘기면 인라인 함수를 호출하는 코드 위치에서는 변수에 저장된 람다의 코드를 알 수 없기 때문에 람다 본문이 인라이닝되지 않는다.

인라인 함수의 한계

인라이닝의 방식으로 인해 람다를 사용하는 모든 함수를 인라이닝할 순 없다. 람다가 본문에 직접 펼쳐지기 때문에 함수가 파라미터로 전달받은 람다를 본문에 사용하는 방식이 한정적일 수밖에 없다(예: 파라미터로 받은 람다를 다른 변수에 저장하고 나중에 사용할 때는 람다를 인라이닝할 수 없음).

인라인 함수의 본문에서 람다 식을 바로 호출하거나 람다 식을 인자로 전달받아 호출하는 경우에는 인라이닝이 가능하다. 그렇지 않다면 컴파일러는 "Illegal usage of inline-parameter"라는 메시지와 함께 인라이닝을 금지시킨다.

NoInline 변경자

둘 이상의 람다를 인자를 받는 함수에서 일부 람다만 인라이닝하고 싶을 때가 있다. 이럴 때는 noinline 변경자를 파라미터 이름 앞에 붙여서 인라이닝을 금지할 수 있다.

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { /* ... */ }

인라이닝을 사용해야 할 때는 언제인가

inline 의 이점을 배우고 나면 이곳저곳 사용해보고 싶지만 그렇게 좋은 생각은 아니다. 인라인 함수는 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 있다. 그렇지 않을 때는 주의깊게 사용하는 것이 좋다.

JVM은 일반 함수 호출의 경우 강력한 인라이닝을 이미 지원한다. 코드 실행을 분석해 가장 이익이 되는 방향으로 호출을 인라이닝한다. 이런 과정은 JIT(바이트코드 -> 기계어) 에서 일어난다. 이런 최적화 때문에 바이트코드에서는 각 함수 구현이 한 번만 있어도 된다. 그러나 인라인 함수를 사용하면 바이트코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 때문에 코드 중복이 생긴다. 그리고 함수를 직접 호출하면 Stack trace가 깔끔해진다.

반면 람다를 인자로 받는 함수를 인라이닝하면 다음과 같은 이점을 얻는다

  1. 인라이닝으로 얻는 부가 비용이 높다: 함수 호출 비용을 줄이고 람다를 표현하는 클래스와 객체를 만들 필요가 없어진다.
  2. 현재의 JVM은 함수 호출과 람다를 인라이닝할 정도로 똑똑하지는 못하다.
  3. Non-local 반환 기능을 사용할 수 있다.

다만 인라인 함수가 코드량이 많을 경우 이걸 모든 호출 지점에 복사하기 때문에 바이트코드 크기가 매우 커질 수 있다.


© 2021. All rights reserved.

Powered by Hydejack v9.1.6