Inline functions — under the hood

Kotlin Vocabulary

Florina Muntenescu
Android Developers

--

You know all of those Util files you create with all sorts of small functions that you end up using a lot throughout your app? If your utility functions get other functions as parameters, chances are you can improve the performance of your app by saving some extra object allocations, that you might not even know you’re making, with one keyword: inline. Let’s see what happens when you pass these short functions around, what inline does under the hood and what you should be aware of when working with inline functions.

Function call — under the hood

Let’s say that you use SharedPreferences a lot in your app so you create this utility function to reduce the boilerplate every time you write something in your SharedPreferences:

fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) {
editor.commit()
} else {
editor.apply()
}
}

Then, you can use it to save a String token:

private const val KEY_TOKEN = “token”class PreferencesManager(private val preferences: SharedPreferences){
fun saveToken(token: String) {
preferences.edit { putString(KEY_TOKEN, token) }
}
}

Now let’s see what’s going on under the hood when preferences.edit is called. If we look at the Kotlin bytecode (Tools > Kotlin > Decompiled Kotlin to Java) we see that there’s a NEW called, so a new object is being created, even if in our code we didn’t call any object constructor:

NEW com/example/inlinefun/PreferencesManager$saveToken$1

Let’s check the decompiled code to make this a bit friendlier. Our saveToken decompiled function is as follows (comments and formatting mine):

Each high-order function we create leads to a Function object creation and memory allocation that introduces runtime overhead.

Inline function — under the hood

To improve the performance of our app we can avoid the new function object creation, by using the inline keyword:

inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) { … }

Now the Kotlin bytecode doesn’t contain any NEW calls and here’s how the decompiled java code looks like for our saveToken method (comments and formatting mine):

Because of the inline keyword, the compiler copies the content of the inline function to the call site, avoiding creating a new Function object.

What to mark as inline

⚠️ If you’re trying to mark as inline a function that doesn’t accept another function as a parameter, you won’t get significant performance benefits and the IDE will even tell you that, suggesting you to remove it:

⚠️ Because inlining may cause the generated code to grow, make sure that you avoid inlining large functions. For example, if you check the Kotlin Standard Library, you’ll see that most of the inlined functions have only 1–3 lines.

⚠️ Avoid inlining large functions!

⚠️ When using inline functions, you’re not allowed to keep a reference to the functions passed as parameter or pass it to a different function — you’ll get a compiler error saying Illegal usage of inline-parameter.

So, for example, let’s modify the edit method and the saveToken method. edit method gets another parameter that is then passed to a different function. saveToken uses a dummy variable that gets updated in the new function:

fun myFunction(importantAction: Int.() -> Unit) {
importantAction(-1)
}
inline fun SharedPreferences.edit(
commit: Boolean = false,
importantAction: Int.() -> Unit = { },
action: SharedPreferences.Editor.() -> Unit
) {
myFunction(importantAction)
...

}
...
fun saveToken(token: String) {
var dummy = 3
preferences.edit(importantAction = { dummy = this}) {
putString(KEY_TOKEN, token)
}
}

We can see that myFunction(importantAction) produces an error:

Here’s how you can solve this, depending on how your function looks like:

Case 1: If you have multiple functions as parameters and you only need to keep a reference to one of them, then you can mark it as noinline.

By using noinline, the compiler will create a new Function object only for that specific function, but the rest will be inlined.

Our edit function will now be:

inline fun SharedPreferences.edit(
commit: Boolean = false,
noinline importantAction: Int.() -> Unit = { },
action: SharedPreferences.Editor.() -> Unit
) {
myFunction(importantAction)
...
}

If we check the bytecode, we see that a NEW call appeared:

NEW com/example/inlinefun/PreferencesManager$saveToken$1

In the decompiled code we can see the following (comments mine):

Case 2: If your function only has one function as a parameter, just prefer not using inline at all. If you do want to use inline, you’d have to mark your parameter with noinline, but like this you’ll have low performance benefits by inlining the method.

To decrease the memory allocations caused by lambda expressions, use the inline keyword! Make sure you apply it to small functions that take a lambda as a parameter. If you need to keep a reference to a lambda or pass it as an argument to another function use the noinline keyword. Start inlining to start saving!

--

--