Skip to content
Minhyung Park

effective kotlin - Use inline modifier for functions with parameters of functional types

kotlin, programming2 min read

TL;DR

  • higher-order 함수에서는 inline 키워드를 사용하는 것이 이점을 준다
  • 무분별한 inline 은 지양하자

inline modifier

  • kotlin stdlib 의 모든 고차원 함수(higher-order function) 들은 inline modifier을 가지고 있다

    • cf.) 고차원 함수 : 함수를 인자로 받거나 리턴값으로 사용할 수 있는 함수

    • example

    • 1inline fun repeat(times: Int, action: (Int) -> Unit) {
      2 for (index in 0 until times) {
      3 action(index)
      4 }
      5}
  • 일반적인 함수는 해당 함수가 불려졌을때 그 함수의 body로 점프하게 됩니다. 해당 함수의 실행이 끝나게 되면 호출되었던 곳으로 다시 돌아가게 됩니다.

  • 반면, inline function의 경우에는 compile 시에 해당 함수를 호출한 곳을 해당 함수의 body로 대체되게 됩니다.

    • ex)

    • 1// before compilation
      2repeat(10) {
      3 println(it)
      4}
      5// after compilation
      6for (index in 0 until 10) {
      7 println(index)
      8}
  • body를 대체함으로 얻을 수 있는 이점

    • Type argument (<T>) 가 구체화될수 있다
    • functional parameter를 가진 함수들의 경우 inline 함수가 더 빠르게 동작한다
    • Non-local 리턴이 허용된다

A type argument can be reified

  • 기본적으로 generic 타입은 컴파일시에 지워지게 됩니다.

    • ex) List<Int> -> List
  • 따라서, 객체의 타입이 List<Int> 인지에 대해서 확인이 불가하게 됩니다.

    • 1any is List<Int> // complie error
      2any is List<*> // ok
      3
      4fun <T> printTypeName() {
      5 print(T::class.simpleName) // error
      6}
  • 하지만 이것은 inline 함수의 경우에는 가능하게 됩니다.

    • 함수 호출 자체가 함수 body로 대체되기 때문에 reified modifier와 함께 사용시 type argument로 사용됩니다
1inline fun <reified T> printTypeName() {
2 print(T::class.simpleName)
3}
4
5// usage
6printTypeName<Int>()
7printTypeName<Char>()
8printTypeName<String>()
9
10// after compilation
11print(Int::class.simpleName)
12print(Char::class.simpleName)
13print(String::class.simpleName)

Functions with functional parameters are faster when they are inlined

인라인 함수의 경우 모든 경우 non inline 함수에 비해 약간씩 빠릅니다. (함수 호출시 함수 본문으로 건너뛰고 다시 돌아오는 과정이 없기 때문에)

이로 인해 kotlin stdlib의 짧은 함수들의 종종 inline 함수로 구현되어있는 이유입니다.

하지만, functional parameter를 가지고 있지 않은 함수라면 유의미한 차이를 가지지 않습니다.

Intellij 에서는 functional parameter 를 가지고 있지 않은 함수를 인라인 하려고 하는 경우 warning을 띄워줍니다

functional parameter를 가진 함수들이 인라인 되었을때 더 빨라지는 이유
  • 함수 자체를 parameter로서 사용하기 위해서는 무엇인가를 잡고 있어야한다.
  • Kotlin/JS 에서는 함수를 일급 시민으로 취급하기 때문에 함수를 함수 그자체 또는 함수 reference로 사용할수 있습니다.
  • 하지만 Kotlin/JVM 에서는 객체가 생성되어야 할 필요가 있다 ( 익명 클래스 또는 일반 클래스로 )

예를 들어

1val lambda: ()->Unit = {
2 // code
3}

위와 같은 람다 표현식은 다음과 같이 컴파일 되게 됩니다.

1Function0<Unit> lambda = new Function0<Unit>() {
2 public Unit invoke() {
3 // code
4 }
5};
6
7// or
8public class Test$lambda implements Function0<Unit> {
9 public Unit invode() {
10 // code
11 }
12}
13Function0 lambda = new Test$lambda();

lambda 표현식의 컴파일 방식

  • ()->Unit -> Function0<Unit>
  • ()->Int -> Function0<Int>
  • (Int)->Int -> Function1<Int,Int>
  • (Int, Int)->Int -> Function2<Int,Int,Int>

따라서 이러한 컴파일 과정이 추가적으로 필요하기 때문에 inline이 아닐경우 더 오래걸리게 됩니다.

또한 로컬 변수를 람다 내에서 가지고 있을 경우에는 inlinenon-inline 과의 차이가 더 커지게 됩니다. non-inline 일 경우 변수의 reference를 넘겨주어야 하기 때문에...

1var l = 1L
2noinlineRepeat(100_000_000) {
3 l += it
4}
5// compile
6val a = Ref.LongRef()
7a.element = 1L
8noinlineRepeat(100_000_000) {
9 a.element = a.element + it
10}

실행 시간의 비교

1@Benchmark
2// on average 189ms
3fun nothingInline(blackhole: Blackhole) {
4 repeat(100_000_000) {
5 blackhole.consume(it)
6 }
7}
8
9@Benchmark
10// on average 447ms
11fun nothingNonInline(blackhole: Blackhole) {
12 noinlineRepeat(100_000_000) {
13 blackhole.consume(it)
14 }
15}
16
17
18// when capture local variable
19
20@Benchmark
21// on average 30ms
22fun nothingInline(blackhole: Blackhole) {
23 var l = 0L
24 repeat(100_000_000) {
25 l += it
26 }
27 blackhole.consume(it)
28}
29
30@Benchmark
31// on average 274ms
32fun nothingNonInline(blackhole: Blackhole) {
33 var l = 0L
34 noinlineRepeat(100_000_000) {
35 l += it
36 }
37 blackhole.consume(it)
38}

Non-local return is allowed

기본적으로 코틀린은 람다 내부에서 return을 허용하지 않습니다.

function literal 들은 계속 이야기 하였듯이 컴파일이 되기 때문에 람다내의 코드는 다른 클래스에 위치하게 됩니다. 따라서, main 을 리턴시킬수 없습니다. 이러한 제한은 inline 되는 것으로 해결할 수 있습니다.

1fun getSomeMoney(): Money? {
2 repeatNoinline(10) {
3 val money = searchForMoney(it)
4 if(money != null) return money // ERROR
5 }
6 return null
7}
8fun getSomeMoney(): Money? {
9 repeat(10) {
10 val money = searchForMoney(it)
11 if(money != null) return money // OK
12 }
13 return null
14}

Crossinline and noinline

몇몇 경우에서 function 자체는 inline 을 원하고 function type argument는 inline을 원하지 않을때 사용할 수 있는 modifier 들이 있다.

  • crossinline
    • function은 inline 되지만 non-local return을 허용하고 싶지 않을 때 사용
  • noinline
    • inline 자체를 원하지 않을때 사용
    • non inline 함수의 argument로 넘길 때 주로 사용하게 된다
1inline fun requestNewToken(
2 hasToken: Boolean,
3 crossinline onRefresh: ()->Unit,
4 noinline onGenerate: ()->Unit
5) {
6 if (hasToken) {
7 httpCall("get-token", onGenerate)
8 } else {
9 httpCall("refresh-token") {
10 onRefresh()
11 onGenerate()
12 }
13 }
14}
15
16fun httpCall(url: String, callback: ()->Unit) {
17 /*...*/
18}

intellij를 사용한다면, 이 두가지 modifier의 정확하게 기억하고 있을 필요는 없다. intellij 가 추천해준다. 다만 이러한 것이 있다는 것 정도만 기억하고 있으면 좋다

Costs of inline modifier

1. inline 함수를 재귀적으로 사용해서는 안된다

1inline fun a() { b() }
2inline fun b() { c() }
3inline fun c() { a() }

해당 호출은 무한적으로 호출이 되고, 코드 작성시에 어떠한 컴파일 에러도 보여주지 않기 때문에 문제를 초래할 수 있다.

2. inline 함수는 더 제한적인 접근 제한자를 가진 element를 사용할수 없다

1inline fun read() {
2 val reader = Reader() // ERROR
3 // ...
4}
5
6private class Reaer {
7 // ...
8}

3. 코드의 양을 비대하게 만들수 있다.

과도한 inline 의 남발은 코드의 양을 굉장히 크게 만들 수 있습니다.

1inline fun printThree() {
2 print(3)
3}
4inline fun threePrintThree() {
5 printThree()
6 printThree()
7 printThree()
8}
9inline fun threeThreePrintThree() {
10 threePrintThree()
11 threePrintThree()
12 threePrintThree()
13}
14inline fun threeThreePrintThree() {
15 threeThreePrintThree()
16 threeThreePrintThree()
17 threeThreePrintThree()
18}
19// after compilation
20inline fun printThree() {
21 print(3)
22}
23inline fun threePrintThree() {
24 print(3)
25 print(3)
26 print(3)
27}
28inline fun threeThreePrintThree() {
29 print(3) // call print(3) * 9 times
30 print(3)
31 ...
32 print(3)
33}
34inline fun threeThreePrintThree() {
35 print(3) // call print(3) * 27 times
36 print(3)
37 ...
38 print(3)
39}