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

유우선·2026년 2월 20일

Kotlin Study📚

목록 보기
27/32

람다의 부가비용?

  • 람다 → 익명 클래스로 컴파일
    • 람다식마다 새로운 클래스가 생김
    • 람다가 변수를 캡처한 경우 호출할 때마다 새로운 객체가 생김
    • 같은 기능을 하는 코드를 직접 실행하는 것보다 효율성이 떨어짐

인라인 함수?

  • inline 변경자를 통해 선언
  • inline 변경자가 선언된 함수를 호출하면 컴파일러는 그 위치에 함수를 구현한 코드를 넣어줌

인라이닝 작동 방식

  • 어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인됨
    • 인라인으로 선언한 함수를 호출하는 코드가 함수 본문을 번역한 바이트코드로 컴파일 됨
  • 인라인 함수에 람다를 인자로 넘기면 람다의 본문도 함께 인라이닝 됨
    • 이 람다로 만들어진 바이트코드는 람다를 인자로 받은 함수의 일부분으로 간주되어 익명 클래스를 생성하지 않음
  • 인라인 함수에 람다 대신 함수 타입의 변수를 넘길 수도 있음
    • 변수에 저장된 람다의 코드를 알 수 없어 람다 본문은 인라이닝 되지 않음
    • 일반적인 람다 호출과 같이 컴파일 됨

인라인 함수의 제약

  • 람다를 사용하는 모든 함수를 인라이닝할 수는 없음
    • 함수 본문에서 파라미터로 받은 람다를 호출하면 인라이닝됨
    • 람다를 변수에 저장하고 그 변수를 호출하면 람다를 표현하는 객체가 존재해야 하기 때문에 인라이닝 할 수 없음
class FunctionStorage {
		var myStorageFunction((Int) -> Unit)? = null
		inline fun storerFunction(f: (Int) -> Unit) {
				myStorageFunction = f // 전달된 파라미터를 저장
		}
}
  • 컴파일러는 Illegal usage of inline-parameter라는 메시지와 함께 인라이닝을 금지시킴

여러 람다 중 일부만 인라이닝 하기

  • noinline 변경자를 파라미터 이름 앞에 붙여 인라이닝을 제한할 수 있음
inline fun foo(inline: () -> Unit, noinline NotInLined: () -> Unit) {
		/* ... */
}

컬렉션 연산 인라이닝

  • 코틀린 표준 라이브러리의 컬렉션 함수 → 대부분 람다를 인자로 받음
    • 이 람다가 성능에 악형향을 끼치는가?
  1. 람다 사용

    data class Person(val name: String, val age: Int)
    
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
    fun main() {
    		println(people.filter { it.age < 30 })
    		// [Person(name=Alice, age=29)]
    }
  2. 직접 기능 구현

    data class Person(val name: String, val age: Int)
    
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
    fun main() {
    		val result = mutableListOf<Person>()
    		for (person in people) {
    				if(person.age > 30) result.add(person)
    		}
    		println(result)
    		// [Person(name=Alice, age=29)]
    }
    • 1번 코드와 2번 코드의 바이트 코드는 거의 동일함
      • filter 함수 → 인라인 함수
      • filter 함수의 바이트코드는 전달된 람다 본문의 바이트코드와 함께 filter를 호출한 위치에 인라이닝 됨
    • 코틀힌 표준 라이브러리에서 제공하는 인라인 함수는 성능 걱정없이 사용해도 무관함

중간 리스트의 부가 비용

fun main() {
		println(
				people.filter { it.age > 30 }
				.map(Person::name)
		)
		// [Bob]
}
  • filter와 map의 본문은 인라이닝됨
  • 하지만 filter에서 걸러낸 결과를 저장하는 중간 리스트를 만듬
  • map에서 이 중간 리스트를 읽어 최종 결과를 만들어냄

처리할 원소가 적다면 중간 리스트의 부가 비용이 크지 않다면 원소가 많아질수록 중간 리스트의 부가 비용을 고려해야 함

asSequence를 통한 부가 비용 감소

  • asSequence를 통해 리스트 대신 시퀀스로 처리하면 중간 리스트로 인한 부가 비용을 줄일 수 있음
    • 각 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현됨
    • 최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출함
  • 시퀀스 연산에서는 람다가 인라이닝 되지 않음
    • 크기가 작은 컬렉션은 일반 컬렉션 연산이 더 성능이 좋을 때도 있음
    • 시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬렉션의 크기가 큰 경우임

인라인 함수는 언제 선언하는가

인라인 키워드를 사용한다고 무조건 성능 개선 효과를 얻는 것은 아님

람다를 인자로 받는 함수만 성능이 좋아질 가능성이 있음

  • 일반 함수 → 인라인 키워드를 사용해도 이익이 거의 없음
    • 일반 함수는 바이트코드에서 각 함수에 대한 구현이 딱 한번만 있으면 됨
    • 함수를 호출하는 부분에서 따로 중복이 발생하지 않음
  • 코틀린 인라인 함수
    • 바이트코드에서 각 함수 호출 지점을 인라인 함수 본문으로 대치하여 코드 중복이 발생함
  • 람다를 인자로 받는 인라인 함수
    • 인라이닝을 통해 없앨 수 있는 부가 비용이 많음
      • 함수 호출 비용을 줄일 수 있음
      • 람다를 표현하는 익명 클래스의 객체를 만들 필요 없음
    • 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있음 (비로컬 return 등)

함수의 크기가 큰 경우 인라인이 비효율적일 수 있음

  • 함수의 바이트코드를 모든 호출 지점에 복사해 넣으면 바이트 코드 전체의 크기가 커질 수 있음

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

람다로 중복을 없앨 수 있는 일반적인 패턴 중 한가지 → 자원 관리

  • 보통 try/finally문으로 자원 획득과 자원 해제를 제어

withLock

  • Lock 인터페이스의 확장 함수
// 코틀린 라이브러리의 withLock 함수 정의
fun <T> Lock.withLock(action: () -> T): T {
		lock()
		try {
				return action()
		} finally {
				unlock()
		}
}

// withLock 사용 방법
val l: Lock = ReentrantLock()
l.withLock{
		// 락에 의해 보호되는 자원 사
}

use 함수

  • 닫을 수 있는 자원(Closeable)에 대해 호출하는 확장 함수
  • 람다를 호출하고 사용 후 자원을 확실하게 닫음
  • use 함수는 인라인 함수임
import java.io.BufferedReader
import java.io.FileReader

fun readFirstLineFromfile(fileName: String): String {
		BufferReader(FileReader(fileReader)).use { br -> 
				return br.readLine()		
		}
}

useLines 함수

  • File, Path 객체에 대해 정의돼 있음
  • 람다가 문자열 시퀀스에 접근하게 해줌
import kotlin.io.path.Path
import kotlin.io.path.useLines

fun readFirstLineFromFile(fileName: String): String {
		Path(fileName).useLines {
				return it.first()
		}
}

코틀린에서는 try-with-resources를 사용하지 말라

  • try-with-resources → 자바에서 Closeable 자원에 사용할 수 있는 구문
static String readFirstLineFromFile(String fileName) throw IOException {
		try(BufferfReader br = new BufferdReader(new FileReader(fileName))) {
				return br.readLine();
		}
}
  • 이 기능을 코틀린에서 use 같은 함수를 통해 간단하게 구현할 수 있음

0개의 댓글