코틀린 & Java/코틀린인액션

인라인 함수: 람다의 부가 비용 없애기

코딩하는후운 2023. 5. 23. 09:29
반응형

인라인 함수: 람다의 부가 비용 없애기

람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생긴다.

이런경우, 실행 시점에 무명 클래스 생성에 따른 부가 비용이 든다.

→ 똑같은 작업을 수행하는 일반 함수를 사용한 구현보다 덜 효율적

inline 변경자

컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트 코드로 바꿔치기 해준다.

작동 방식

함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일한다.

[Kotlin in Action 책]
p.365
8.13~8.3
  • synchronized 함수의 본문뿐 아니라 전달된 람다의 본문도 함께 인라이닝 된다.

함수 타입의 변수를 넘길수도 있다.

fun runUnderLock(body: () -> Unit) {
	synchronized(lock, body)
}
  • 이런 경우에는 람다 본문은 인라이닝 되지않고 synchronized 함수의 본문만 인라이닝 된다.

: 호출하는 코드 위치에서는 변수에 저장된 람다의 코드를 알 수 없기 때문.

인라인 함수의 한계

  • 람다식의 본문은 결과코드에 직접 들어갈 수 있다.
  • 파라미터로 전달받은 람다를 본문에 사용하는 방식이 한정될 수밖에 없다.
  • 파라미터로 받은 람다를 다른변수에 저장하고 사용한다면 람다를 표현하는 객체가 어딘가는 존재해야 하기 때문에 람다를 인라이닝 할 수 없다.
fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
	return TransformingSequence(this, transform)
}

전달받은 transform을 호출하지 않는 대신, 클래스의 생성자에게 그 함수값을 넘긴다.

생성자는 전달 받은 람다를 프로퍼티로 저장한다.

transform을 함수 인터페이스를 구현하는 무명클래스 인스턴스로 만들어야만 한다.

→ 일반적인(인라이닝하지 않는)함수로 표현

noinline 변경자 9.2.4에서 좀더 나옴!

인라이닝 하면 안되는 람다를 파라미터로 받을 때 사용

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

컬렉션 연산 인라이닝

  • 표준 라이브러리의 컬렉션 함수는 대부분 람다를 인자로 받는다.
  • ex) filter 함수는 인라인 함수다
    • filter함수의 바이트코드는 그 함수에 전달된 람다 본문의 바이트코드와 함께 filter를 호출한 위치에 들어간다.

: 결과 직접짠 코드와 거의 같다. → 코틀린이 제공하는 함수 인라이닝을 믿고 성능에 신경 쓰지 않아도 된다.

filter와 map을 연쇄해서 사용하면?

  • 둘다 인라인 함수다. 추가 객체나 클래스 생성은 없다.
  • 하지만 리스트를 걸러낸 결과를 저장하는 중간 리스트를 만든다.
  • filter함수에서 만들어진 코드는 원소를 중간리스트에 추가하고, map함수에서 만들어진 코드는 그 중간 리스트를 읽어서 사용한다.

처리할 원소가 많아지면 부가 비용이 커진다.

  • asSequence를 통해 중간 리스트로 인한 부가 비용을 줄일 수 있다.
    • 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현
    • 최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출한다.

→ 시퀀스는(람다를 저장해야 하므로) 람다를 인라인 하지 않는다.

그렇다고 모든 컬렉션 연산에 asSequence를 붙여서는 안된다.

  • 크기가 작은 컬렉션은 일반 연산이 더 성능이 나을 수 있다.

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

  • 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다.
    • 일반 함수의 경우 JVM은 이미 강력하게 인라이닝을 지원.
    • JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝 한다.
    • JVM의 최적화를 활용한다면 바이트코드에서는 각 함수구현이 정확히 한 번만 있으면 되고, 호출하는 부분에서 따로 함수 코드를 중복할 필요가 없다.
  • 반면 코틀린 인라인 함수는 바이트코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 때문에 코드 중복이 생긴다.
  • 람다를 인자로 받는 함수를 인라이닝하면 이익이 더 많다
    • 인라이닝을 통해 없앨 수 있는 부가 비용이 상당하다.
      • 함수 호출 비용을 줄일 수 있다.
      • 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요도 없어진다.
    • JVM은 함수 호출과 람다를 인라이닝해 줄 정도로 똑똑하지 못하다.
    • 일반 람다에서는 사용할 수 없는 몇가지 기능을 사용할 수 있다.
      • 넌로컬(non-local) 반환 - 8장 뒤에서 나옴!

주의 : 코드 크기에 주의.

  • 함수가 크면 바이트 코드가 전체적으로 아주 커질 수 있다
    • 다른 inline 함수를 보면 모두 크기가 작다.

자원 관리를 위해 인라인 된 람다 사용

람다로 중복을 없앨 수 있는 일반적인 패턴중 한 가지는 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 자원 관리다.

  • 자원(resource)은 파일, 락, 데이터베이스 트랜잭션 등
  • 코틀린 표준라이브러리에는, 자바 try-with-resource와 같은 기능을 제공하는 use라는 함수가 있음.

use함수는 Closeable을 구현한 객체에 한해서만 사용할 수 있다.

use는 람다를 호출한 다음에 자원을 닫아준다.

  • 람다안에서 예외가 발생한 경우에도 자원을 확실히 닫는다.
반응형