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.
Table of Contents
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
- Inline functions , Kotlin Docs.