Kotlin Inline Functions
Inline functions
Using higher-order functions introduces certain runtime penalties. Each function is an object, and it can capture a closure, that is, variables accessed from the function body. Memory allocation for both function objects and classes, as well as virtual calls, introduces runtime overhead.
In such cases, much of the overhead can be removed by marking lambdas as inline. The function shown below is a good example of this situation. The lock() function can easily be inlined at the call site. Consider the following case.
lock(l) { foo() }
Instead of creating a function object for the parameter and call, the compiler produces code like this.
l.lock()
try {
foo()
}
finally {
l.unlock()
}
To make the compiler do this work, specify the inline modifier on the lock() function as follows.
inline fun lock<T>(lock: Lock, body: () -> T): T {
// ...
}
The inline modifier affects both the function itself and the lambdas passed to it. They are inlined at every call site.
Inlining can make generated code larger, but if it is used reasonably, for example by not inlining large functions, it can improve performance, especially around “megamorphic” call sites inside loops.
Converting inline functions to bytecode
The content so far is based on the official documentation for inline functions. It may not be easy to understand from the description alone, but it becomes clearer when you check the compiled bytecode.
The following examples compare bytecode when inline is declared on a function and when it is not.
The code below does not declare inline on the calc function.
fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int {
println("called calc function")
return op(a, b)
}
fun main() {
var result = calc(2, 3) { a: Int, b: Int -> a + b }
println(result)
}
Output:
called calc function
5
The bytecode-converted form of the code above is as follows.
public static final int calc(int a, int b, @NotNull Function2 op) {
Intrinsics.checkNotNullParameter(op, "op");
String var3 = "called calc function";
boolean var4 = false;
System.out.println(var3);
return ((Number)op.invoke(a, b)).intValue();
}
public static final void main() {
int result = calc(2, 3, (Function2)null.INSTANCE);
boolean var1 = false;
System.out.println(result);
}
This time, declare the inline modifier on the calc function as follows.
inline fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int {
println("called calc function")
return op(a, b)
}
Now convert it to bytecode again and check the result.
public static final int calc(int a, int b, @NotNull Function2 op) {
int $i$f$calc = 0;
Intrinsics.checkNotNullParameter(op, "op");
String var4 = "called calc function";
boolean var5 = false;
System.out.println(var4);
return ((Number)op.invoke(a, b)).intValue();
}
public static final void main() {
byte a$iv = 2;
int b$iv = 3;
int $i$f$calc = false;
String var4 = "called calc function";
boolean var5 = false;
System.out.println(var4);
int var8 = false;
int result = a$iv + b$iv;
boolean var9 = false;
System.out.println(result);
}
Comparing the two bytecode snippets above, the source of the calc function is converted in a very similar way, but the main function is different. When the function is not inline, a Function2 object is created and called. When the function is inline, the code contents of calc are copied into the call site.
The result of both code examples is naturally the same: 5 is printed.
In this way, inline can compile a function as an inline function. The program size grows, but the overhead of function calls is reduced and processing becomes faster. This is effective when iterating over collection elements, for example.
noinline
Lambdas passed to inline functions are also inlined, but by specifying the noinline modifier, you can prevent only that lambda from being inlined.
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}
Inlinable lambdas can be called only inside inline functions or passed as inlinable parameters. However, noinline lets you handle them as desired, for example by storing them in a field or passing them somewhere else.
If an inline function has no inlinable function parameters and no reified type parameters, the compiler issues a warning because marking such a function as inline is meaningless. If you still need to use it, add
@Suppress("NOTHING_TO_INLINE")to suppress the warning.
Converting noinline lambdas to bytecode
Again, use a simple example and convert it to bytecode to check the noinline modifier.
In the example below, the lambda parameter op of an inline function is declared with the noinline modifier.
inline fun calc2(a: Int, b: Int, noinline op: (Int, Int) -> Int): Int {
println("called calc function")
return op(a, b)
}
fun main() {
var result = calc2(2, 3) { a: Int, b: Int -> a + b }
println(result)
}
Convert the code above, where the lambda is declared noinline, to bytecode.
public static final int calc2(int a, int b, @NotNull Function2 op) {
int $i$f$calc2 = 0;
Intrinsics.checkNotNullParameter(op, "op");
String var4 = "called calc function";
boolean var5 = false;
System.out.println(var4);
return ((Number)op.invoke(a, b)).intValue();
}
public static final void main() {
byte a$iv = 2;
byte b$iv = 3;
Function2 op$iv = (Function2)null.INSTANCE;
int $i$f$calc2 = false;
String var5 = "called calc function";
boolean var6 = false;
System.out.println(var5);
int result = ((Number)op$iv.invoke(Integer.valueOf(a$iv), Integer.valueOf(b$iv))).intValue();
boolean var7 = false;
System.out.println(result);
}
Now remove the noinline modifier and convert it to bytecode.
inline fun calc2(a: Int, b: Int, op: (Int, Int) -> Int): Int {
println("called calc function")
return op(a, b)
}
public static final int calc2(int a, int b, @NotNull Function2 op) {
int $i$f$calc2 = 0;
Intrinsics.checkNotNullParameter(op, "op");
String var4 = "called calc function";
boolean var5 = false;
System.out.println(var4);
return ((Number)op.invoke(a, b)).intValue();
}
public static final void main() {
byte a$iv = 2;
int b$iv = 3;
int $i$f$calc2 = false;
String var4 = "called calc function";
boolean var5 = false;
System.out.println(var4);
int var8 = false;
int result = a$iv + b$iv;
boolean var9 = false;
System.out.println(result);
}
Comparing the bytecode above, when the parameter is declared with the noinline modifier, a Function2 object is declared and called directly, and the body of the calc2 function is not copied. In contrast, in the bytecode without the noinline modifier, the body of the lambda function (a$iv + b$iv) is copied and executed.
Non-local returns
In Kotlin, an unlabeled return is usually used to exit a named function or anonymous function, but a label must be used to exit a lambda. Because a lambda cannot return from the enclosing function, using only return inside a lambda is prohibited.
fun ordinaryFunction(block: () -> Unit) {
println("Hello!!")
}
fun main() {
ordinaryFunction {
return // ERROR: cannot return from here.
}
}
However, if the lambda is passed to an inline function, the return can also be inlined, so this is allowed.
inline fun inlinedFunction(block: () -> Unit) {
println("Hello!!")
}
fun main() {
inlinedFunction {
return // OK: the lambda is inlined.
}
}
A return that is inside a lambda but exits the enclosing function is called a non-local return. This kind of construct commonly occurs in loops wrapped by inline functions.
fun hasZeros(ints: List<Int>): Boolean {
ints.forEach {
if (it == 0) return true // Returns from hasZeros.
}
return false
}
Some inline functions may call the passed lambda not directly in the function body, but from another execution context such as a local object or nested function. In such cases, non-local control flow is not allowed from the lambda. To handle this, mark the lambda parameter with the crossinline modifier.
inline fun f(crossinline body: () -> Unit) {
Runnable {
body()
}.run()
// ...
}
fun main() {
f { println("Hello!")}
}
breakandcontinuecannot yet be used in inline lambdas, but support is planned.
Reified type parameters
Sometimes you need to access the type passed as a parameter.
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p.parent
}
@Suppress("UNCHECKED_CAST")
return p as T?
}
Here, reflection is used while walking the tree to check whether a node has a specific type. There is no real problem, but the call site does not look very clean.
treeNode.findParentOfType(MyTreeNode::class.java)
A better solution is simply to pass the type to the function. It can be called as follows.
treeNode.findParentOfType<MyTreeNode>()
To use this feature, inline functions support reified type parameters, so you can write the following.
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
In the code above, the type parameter is qualified with the reified modifier so it can be accessed inside the function almost like a normal class. Because the function is inline, reflection is not needed, and normal operators such as !is and as can now be used. You can also call the function as treeNode.findParentOfType<MyTreeNodeType>().
In many cases, reflection may not be necessary, but it can still be used with reified type parameters.
inline fun <reified T> membersOf() = T::class.members
fun main(s: Array<String>) {
println(membersOf<StringBuilder>().joinToString("\n"))
}
Normal functions that are not marked inline cannot have reified parameters. Types that do not have runtime representation, such as non-reified type parameters or fictional types like Nothing, cannot be used as arguments for a reified parameter.
Inline properties
The inline modifier can be used on accessors of properties that do not have backing fields. It can be specified on individual property accessors.
val foo: Foo
inline get() = Foo()
var bar: Bar
get() = ...
inline set(v) { ... }
You can also declare the entire property as inline so both accessors are inlined.
inline var bar: Bar
get() = ...
set(v) { ... }
At call sites, inline accessors are inlined like regular inline functions.
Restrictions for public API inline functions
An inline function is considered part of a module’s public API unless it is declared public, protected, private, or internal. It can be called from other modules and is also inlined at those call sites.
This creates a risk of binary incompatibility caused by changes to the module that declares the inline function if the calling module is not recompiled afterward.
To remove the risk of such incompatibility caused by changes to a module’s non-public API, public API inline functions cannot use private and internal declarations, or private API declarations in their bodies.
An internal declaration can be annotated with @PublishedApi so it can be used from API inline functions. When an internal inline function is declared with @PublishedApi, its body is checked as if it were public.