Koltin: Inline Functions Tutorial

Photo by Connor Mollison
Photo by Connor Mollison
Passing lambdas to functions in Kotlin can cause runtime overhead. Correct use of the inline modifier can remove these runtime overhead and improve performance.

Passing lambdas to functions in Kotlin can cause runtime overhead. Correct use of the inline modifier can remove these runtime overhead and improve performance. This article explain how to use the inline modifier.

Inline Functions

Using lambdas causes runtime overhead. In the code below, we call compute() 10 times, passing a lambda to compute() each time.

fun compute(x: Int, block: (Int) -> Int): Int {
    return block(x)
}

fun main() {
    var times = 0
    var total = 0
    for (n in 0 until 10) {
        total += compute(n) {
            times++
            it * it
        }
    }
    println(times)
}

Below is the Java code generated by Kotlin. Each time compute() is called, an anonymous Function1 object is created. In addition, because the Function1 object needs to access the outside variable times, it also needs to capture a closure. So these two runtime overheads are generated every time compute() is called. If compute() is not called very often, we can ignore these small runtime overheads. But if compute() is called all the time in a loop, the accumulated runtime overhead may affect the performance.

public class MainKt {
    public static int compute(int x, @NotNull Function1 block) {
        return ((Number) block.invoke(x)).intValue();
    }

    public static void main() {
        final Ref.IntRef times = new Ref.IntRef();
        times.element = 0;
        int total = 0;
        int n = 0;

        for (byte var3 = 10; n < var3; ++n) {
            total += compute(n, new Function1() {
                public Object invoke(Object var1) {
                    return this.invoke(((Number) var1).intValue());
                }

                public int invoke(int it) {
                    int var10001 = times.element++;
                    return it * it;
                }
            });
        }

        n = times.element;
        System.out.println(n);
    }

    public static void main(String[] var0) {
        main();
    }
}

The inline modifier is the solution proposed by Kotlin. In the following code, we use the inline modifier to modify compute() .

inline fun compute(x: Int, block: (Int) -> Int): Int {
    return block(x)
}

fun main() {
    var times = 0
    var total = 0
    for (n in 0 until 10) {
        total += compute(n) {
            times++
            it * it
        }
    }
    println(times)
}

Below is the Java code generated by Kotlin. We found that compute() is still declared, but not called. The code calling compute() in the loop is replaced by the contents of the lambda. Therefore, the inline modifier helps us remove the runtime overhead of calling compute(), creating the Function1 object, and capturing a closure, etc. to improve performance.

public class MainKt {
    public static int compute(int x, @NotNull Function1 block) {
        return ((Number) block.invoke(x)).intValue();
    }

    public static void main() {
        int times = 0;
        int total = 0;
        int n = 0;

        for (byte var3 = 10; n < var3; ++n) {
            ++times;
            int var9 = n * n;
            total += var9;
        }

        System.out.println(times);
    }

    public static void main(String[] var0) {
        main();
    }
}

However, the inline modifiers may also grow the generated code. In addition, we need avoid the code of the inline function being too long. Because if the code in the loop is too long, it will cause megamorphic and reduce performance.

Non-Local Returns

Non-local returns refer to a return in lambdas that do not specify a label. Non-local returns are used to leave the enclosing function. In the following code, nonInlineLoop() is an ordinary function. In lambda, only return@nonInlineLoop can be used to exit nonInlineLoop(), but return cannot be used to exit main().

fun nonInlineLoop(array: List<Int>, block: (Int) -> Unit) {
    for (element in array) {
        block(element)
    }
}

fun main() {
    nonInlineLoop(listOf(1, 2, 3)) {
        if (it % 2 == 0) {
            return@nonInlineLoop // OK
        }
        println(it)
    }

    nonInlineLoop(listOf(1, 2, 3)) {
        if (it % 2 == 0) {
            return // ERROR: A non-local return is not allowed
        }
        println(it)
    }
}

But if it is inline functions, we can use non-local returns in lambdas to exit the enclosing function. In the following code, we can use return@inlineLoop to leave inlineLoop(), and we can also use return to exit main().

inline fun inlineLoop(array: List<Int>, block: (Int) -> Unit) {
    for (element in array) {
        block(element)
    }
}

fun main() {
    inlineLoop(listOf(1, 2, 3)) {
        if (it % 2 == 0) {
            return@inlineLoop // OK
        }
        println(it)
    }

    inlineLoop(listOf(1, 2, 3)) {
        if (it % 2 == 0) {
            return // OK: A non-local return is allowed
        }
        println(it)
    }
}

noinline Modifier

Kotlin will automatically inline all lambda parameters of an inline function. But if you don’t want a parameter to be inlined, you can use the noinline modifier, as follows.

inline fun compute(x: Int, block1: (Int) -> Int, noinline block2: (Int) -> Int): Int {
    ...
}

corssinline Modifier

When a lambda you pass to an inline function is not directly called in that inline function, but is called in other execution contexts, such as a local object or a nested function, compilation errors will occur. Because in this case, the lambda cannot use non-local returns to exit the enclosing function.

We can use the crossinline modifier to indicate that the lambda cannot use non-local returns. In this way, it can be successfully compiled, as the following code.

interface Compute {
    fun exe(x: Int): Int
}

inline fun loop(array: List<Int>, crossinline computeBlock: (Int) -> Int, block: (Int) -> Unit) {
    for (element in array) {
        val compute = object : Compute {
            override fun exe(x: Int): Int = computeBlock(x)
        }
        block(compute.exe(element))
    }
}

fun main() {
    loop(listOf(1, 2, 3), { it * it }) {
        if (it % 2 == 0) {
            return@loop
        }
        println(it)
    }
}

reified Modifier

When an inline function is a generic function, we can modify the type parameter with the reified modifier. In this way, we can use reflection on the type parameter.

inline fun <reified T> String.numeric(): T {
    return when (T::class) {
        Int::class -> this.toInt() as T
        Long::class -> this.toLong() as T
        Float::class -> this.toFloat() as T
        Double::class -> this.toDouble() as T
        else -> throw UnsupportedOperationException()
    }
}

fun main() {
    val i: Int = "1".numeric()
    println(i)
    val d: Double = "3.14".numeric()
    println(d)
}

Inline Properties

In Kotlin, when declaring a property in a class, Kotlin will generate a backing field, a getter and a setter for it. We can use the inline modifier to indicate not to generate backing fields.

We can add the inline modifier before the getter or setter, or before the property.

class Person(var name: String) {
    inline var displayName: String
        get() = name
        set(value) {
            field = value // ERROR: No backing field
            name = value
        }

    var firstName: String
        get() = name
        inline set(value) {
            field = value // ERROR: No backing field
            name = value
        }

    val lastName: String
        inline get() = name
}

Conclusion

In fact, when writing Kotin programs, we call a lot of inline functions. Because many of Kotlin’s standard functions are actually inline functions, such as let(), apply(), forEach(), filter(), and so on. They allow us to use functional programming without reducing performance. But as mentioned in this article, too long lambda will cause megamorphic and reduce performance. So, after reading this article, in addition to knowing how to create inline functions, you also know the importance of keeping lambdas short.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like