when to use an inline function in Kotlin?

Let's say you create a higher order function that takes a lambda of type () -> Unit (no parameters, no return value), and executes it like so:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

In Java parlance, this will translate to something like this (simplified!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

And when you call it from Kotlin...

nonInlined {
    println("do something here")
}

Under the hood, an instance of Function will be created here, that wraps the code inside the lambda (again, this is simplified):

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

So basically, calling this function and passing a lambda to it will always create an instance of a Function object.


On the other hand, if you use the inline keyword:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

When you call it like this:

inlined {
    println("do something here")
}

No Function instance will be created, instead, the code around the invocation of block inside the inlined function will be copied to the call site, so you'll get something like this in the bytecode:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

In this case, no new instances are created.


Let me add: When not to use inline:

  1. If you have a simple function that doesn't accept other functions as an argument, it does not make sense to inline them. IntelliJ will warn you:

    Expected performance impact of inlining '...' is insignificant. Inlining works best for functions with parameters of functional types

  2. Even if you have a function "with parameters of functional types", you may encounter the compiler telling you that inlining does not work. Consider this example:

     inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
         val o = operation //compiler does not like this
         return o(param)
     }
    

    This code won't compile, yielding the error:

    Illegal usage of inline-parameter 'operation' in '...'. Add 'noinline' modifier to the parameter declaration.

    The reason is that the compiler is unable to inline this code, particularly the operation parameter. If operation is not wrapped in an object (which would be the result of applying inline), how can it be assigned to a variable at all? In this case, the compiler suggests making the argument noinline. Having an inline function with a single noinline function does not make any sense, don't do that. However, if there are multiple parameters of functional types, consider inlining some of them if required.

So here are some suggested rules:

  • You can inline when all functional type parameters are called directly or passed to other inline function
  • You should inline when ^ is the case.
  • You cannot inline when function parameter is being assigned to a variable inside the function
  • You should consider inlining if at least one of your functional type parameters can be inlined, use noinline for the others.
  • You should not inline huge functions, think about generated byte code. It will be copied to all places the function is called from.
  • Another use case is reified type parameters, which require you to use inline. Read here.

Use inline for preventing object creation

Lambdas are converted to classes

In Kotlin/JVM, function types (lambdas) are converted to anonymous/regular classes that extend the interface Function. Consider the following function:

fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

The function above, after compilation will look like following:

public static final void doSomethingElse(Function0 lambda) {
    System.out.println("Doing something else");
    lambda.invoke();
}

The function type () -> Unit is converted to the interface Function0.

Now let's see what happens when we call this function from some other function:

fun doSomething() {
    println("Before lambda")
    doSomethingElse {
        println("Inside lambda")
    }
    println("After lambda")
}

Problem: objects

The compiler replaces the lambda with an anonymous object of Function type:

public static final void doSomething() {
    System.out.println("Before lambda");
    doSomethingElse(new Function() {
            public final void invoke() {
            System.out.println("Inside lambda");
        }
    });
    System.out.println("After lambda");
}

The problem here is that, if you call this function in a loop thousands of times, thousands of objects will be created and garbage collected. This affects performance.

Solution: inline

By adding the inline keyword before the function, we can tell the compiler to copy that function's code at call-site, without creating the objects:

inline fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

This results in the copying of the code of the inline function as well as the code of the lambda() at the call-site:

public static final void doSomething() {
    System.out.println("Before lambda");
    System.out.println("Doing something else");
    System.out.println("Inside lambda");
    System.out.println("After lambda");
}

This doubles the speed of the execution, if you compare with/without inline keyword with a million repetitions in a for loop. So, the functions that take other functions as arguments are faster when they are inlined.


Use inline for preventing variable capturing

When you use the local variables inside the lambda, it is called variable capturing(closure):

fun doSomething() {
    val greetings = "Hello"                // Local variable
    doSomethingElse {
        println("$greetings from lambda")  // Variable capture
    }
}

If our doSomethingElse() function here is not inline, the captured variables are passed to the lambda via the constructor while creating the anonymous object that we saw earlier:

public static final void doSomething() {
    String greetings = "Hello";
    doSomethingElse(new Function(greetings) {
            public final void invoke() {
            System.out.println(this.$greetings + " from lambda");
        }
    });
}

If you have many local variables used inside the lambda or calling the lambda in a loop, passing every local variable through the constructor causes the extra memory overhead. Using the inline function in this case helps a lot, since the variable is directly used at the call-site.

So, as you can see from the two examples above, the big chunk of performance benefit of inline functions is achieved when the functions take other functions as arguments. This is when the inline functions are most beneficial and worth using. There is no need to inline other general functions because the JIT compiler already makes them inline under the hood, whenever it feels necessary.


Use inline for better control flow

Since non-inline function type is converted to a class, we can't write the return statement inside the lambda:

fun doSomething() {
    doSomethingElse {
        return    // Error: return is not allowed here
    }
}

This is known as non-local return because it's not local to the calling function doSomething(). The reason for not allowing the non-local return is that the return statement exists in another class (in the anonymous class shown previously). Making the doSomethingElse() function inline solves this problem and we are allowed to use non-local returns because then the return statement is copied inside the calling function.


Use inline for reified type parameters

While using generics in Kotlin, we can work with the value of type T. But we can't work with the type directly, we get the error Cannot use 'T' as reified type parameter. Use a class instead:

fun <T> doSomething(someValue: T) {
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // Error
}

This is because the type argument that we pass to the function is erased at runtime. So, we cannot possibly know exactly which type we are dealing with.

Using an inline function along with the reified type parameter solves this problem:

inline fun <reified T> doSomething(someValue: T) {
    println("Doing something with value: $someValue")               // OK
    println("Doing something with type: ${T::class.simpleName}")    // OK
}

Inlining causes the actual type argument to be copied in place of T. So, for example, the T::class.simpleName becomes String::class.simpleName, when you call the function like doSomething("Some String"). The reified keyword can only be used with inline functions.


Avoid inline when calls are repetitive

Let's say we have the following function that is called repetitively at different abstraction levels:

inline fun doSomething() {
    println("Doing something")
}

First abstraction level

inline fun doSomethingAgain() {
    doSomething()
    doSomething()
}

Results in:

public static final void doSomethingAgain() {
    System.out.println("Doing something");
    System.out.println("Doing something");
}

At first abstraction level, the code grows at: 21 = 2 lines.

Second abstraction level

inline fun doSomethingAgainAndAgain() {
    doSomethingAgain()
    doSomethingAgain()
}

Results in:

public static final void doSomethingAgainAndAgain() {
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
}

At second abstraction level, the code grows at: 22 = 4 lines.

Third abstraction level

inline fun doSomethingAgainAndAgainAndAgain() {
    doSomethingAgainAndAgain()
    doSomethingAgainAndAgain()
}

Results in:

public static final void doSomethingAgainAndAgainAndAgain() {
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
    System.out.println("Doing something");
}

At third abstraction level, the code grows at: 23 = 8 lines.

Similarly, at the fourth abstraction level, the code grows at 24 = 16 lines and so on.

The number 2 is the number of times the function is called at each abstraction level. As you can see the code grows exponentially not only at the last level but also at every level, so that's 16 + 8 + 4 + 2 lines. I have shown only 2 calls and 3 abstraction levels here to keep it concise but imagine how much code will be generated for more calls and more abstraction levels. This increases the size of your app. This is another reason why you shouldn't inline each and every function in your app.


Avoid inline in recursive cycles

Avoid using the inline function for recursive cycles of function calls as shown in the following code:

// Don't use inline for such recursive cycles

inline fun doFirstThing() { doSecondThing() }
inline fun doSecondThing() { doThirdThing() }
inline fun doThirdThing() { doFirstThing() }

This will result in a never ending cycle of the functions copying the code. The compiler gives you an error: The 'yourFunction()' invocation is a part of inline cycle.


Can't use inline when hiding implementation

The public inline functions cannot access private functions, so they cannot be used for implementation hiding:

inline fun doSomething() {
    doItPrivately()  // Error
}

private fun doItPrivately() { }

In the inline function shown above, accessing the private function doItPrivately() gives an error: Public-API inline function cannot access non-public API fun.


Checking the generated code

Now, about the second part of your question:

but I found that there is no function object created by kotlin for a non-inline function. why?

The Function object is indeed created. To see the created Function object, you need to actually call your lock() function inside the main() function as follows:

fun main() {
    lock { println("Inside the block()") }
}

Generated class

The generated Function class doesn't reflect in the decompiled Java code. You need to directly look into the bytecode. Look for the line starting with:

final class your/package/YourFilenameKt$main$1 extends Lambda implements Function0 { }

This is the class that is generated by the compiler for the function type that is passed to the lock() function. The main$1 is the name of the class that is created for your block() function. Sometimes the class is anonymous as shown in the example in the first section.

Generated object

In the bytecode, look for the line starting with:

GETSTATIC your/package/YourFilenameKt$main$1.INSTANCE

INSTANCE is the object that is created for the class mentioned above. The created object is a singleton, hence the name INSTANCE.


That's it! Hope that provides useful insight into inline functions.