inline, noinline, crossinline. What are they? — Kotlin The Series
Kotlin has these 3 modifiers, inline
, noinline
, crossinline
. In this article, we will discuss these 3 modifiers. What are they, Where are they used, When they should be used, Why we should use them, and How to use them. As we talk further in Kotlin, we should also have a basic understanding of Java to cross-validate the result.
HOF (Higher-Order Function)
Before we continue, let’s do a quick refresh on what is higher-order function even means. https://en.wikipedia.org/wiki/Higher-order_function
A higher-order function is a function that takes functions as parameters or returns a function.
Let’s take a look at the simple code example here
The function twice
takes a function and returns another function. we name our function parameter as fx
. fx
takes an Integer type, and returns an Integer type. the returning function from twice
will process an integer as a parameter and return the value from calling fx
within fx
.
In term of programming, an anonymous function or lambda expression is a function definition that is not bound to an identifier or a function name declaration. It can be passed around as parameter on HOF, or as a return of HOF. https://en.wikipedia.org/wiki/Anonymous_function
Using HOF will cause certain runtime penalties, because each function is an object or instance, and the function itself captures a closure. A closure is a scope of variables that can be accessed in the body of the function. It is mentioned in the Kotlin Docs. Memory allocations and virtual calls introduce runtime overhead.
Closures in functional programming are the functions that are aware of their surroundings. A closure has access to the variables and parameters defined in the outer scope.
We will use HOF as a reference since inline
, noinline
, and crossinline
will rely a lot on this paradigm. By knowing the drawbacks of HOF, we can already have an assumption on the reason why Kotlin introduces these 3 keywords, and why we should use them.
inline modifier
inline
modifier tells the compiler to inline both the closure and all passed lambda expressions onto each of the function callers. By definition, we kept our ordinary function or HOF code, but the compiler will generate code for us as the function body is a part of the caller site.
Let’s understand more with an example. We will use the previous code as a reference for our next example.
We add inline
modifier toplusTen
function. This will tell the compiler to inline i + 10
onto the caller site. When we decompile this into a Java source, we will get something like
public static final int plusTen(int i) {
return i + 10;
}
public static final void main() {
int i = 11;
int result = i + 10;
System.out.println(result);
}
The closure from plusTen
is inlined onto the main
function. This is an example of when we should avoid using inline
modifiers. What would be the reason for inlining such simple closure onto the caller? The generated code grows caused by this inline
modifier, but the performance impact is insignificant. We will receive a warning from the compiler that says, Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
. And that is HOF that we already talked about earlier, a function with parameters of functional types.
Steps to decompile from Kotlin bytecodes into Java source file in intellij:
1. Open up Tools > Kotlin > Show Kotlin Bytecode. You will get the bytecode of your Kotlin file.
2. Click the Decompile to generate the Java code from the Kotlin bytecode.
If we remove the inline
modifier from plusTen
we will get the Java source like the following:
public static final int plusTen(int i) {
return i + 10;
}
public static final void main() {
int result = plusTen(11);
System.out.println(result);
}
Calling an ordinary function like that doesn’t get the actual benefit that the inline modifier gives. Now let’s modify the function a bit so that it gets the actual benefit of an inline modifier. We will add a new HOF named applyThenPrint
Our HOF applyThenPrint
meant to extract our printing use case so that it can lazily print the result of any function with type(int) -> Int
. In this case, we use the Kotlin extension function to be inlined. The Java source result will be something like the following:
public static final int plusTen(int i) {
return i + 10;
}
public static final void applyThenPrint(int extensionValue, @NotNull Function1 fx) {
int var3 = ((Number)fx.invoke(extensionValue)).intValue();
System.out.println(var3);
}
public static final void main() {
int var1 = 11;
int var2 = plusTen(var1);
System.out.println(var2);
}
That is the result of our inlined function. No applyThenPrint
execution on our main function, thus we remove the need to instantiate a Function1
object and directly place the logic of our inlined function onto the main function.
As our HOF already has a specific domain use case for lazily printing the anonymous function, let’s take a look if we have another number manipulator function, called plusFifty
.
If you already understand this concept, you will know what the Java source will look like. Imagine our HOF contains a large and complex code, how huge the bytecode will become. In that case, is it still good to use inline
? You can freely write down your answer in this article's comment section and discuss it together.
Now we can understand better and get the answer of what inline
is, when, where, why, and how inline
should be used.
noinline modifier
Understanding noinline
requires inline
to be understood first. As we reach this section, we already have the basic requirements to move forward.
What is a noinline
in Kotlin? Assume that we prevent some lambda function on our HOF param from being inlined. This case is what noinline
used for. We mark the parameters with noinline
modifier.
Let’s modify our previous use case. We will add one more parameter that is used as a Tag Producer, and we mark it as nullable.
This way, the compiler will refuse if we remove the noinline
modifier on the lambda parameter. That is because inline
requires a non-null param. The result in Java is as below:
public static final void main() {
int var1 = 11;
Function1 tagProducer = new Function1() {
@Override
public String apply(Integer it) {
return "MY_TAG("+ it + ")")
}
};
String tagWrapperText = tagProducer == null ?
"" : (String)tagProducer.invoke(Integer.valueOf(var1)) + ": ";
StringBuilder builder = (new StringBuilder())
.append(tagWrapperText);
int var3 = plusTen(var1);
String var4 = builder.append(var7).toString();
System.out.println(var4);
}
We see that noinline
lambda instantiated as a Function instance, and fx is still written to the calling function.
Another case when noinline
is necessary when the lambda function needs to be manipulated, or passed around. inlinable lambdas can only be called inside inline functions or passed as inlinable arguments.
We have enriched our knowledge so far with inline
and noinline
. We cannot add noinline
modifier without inline
modifier set.
crossinline modifier
This one will be a bit tricky. Understanding crossinline
requires inline
to be understood first. In addition, we need to know the basic concept of non-local returns. Let’s do a quick review of what non-local return is.
In Kotlin, we can only use a normal, unqualified return
to exit a named function or an anonymous function. To exit a lambda, we must use a label (e.g return@someLabel
). A normal return
inside a lambda is forbidden, because a lambda cannot make the enclosing function return
.
Here, we use a new simple example, having a normal unqualified return
on lambda. Look normal, we explicitly add return
on the last part of the lambda. But that is not the way it works. The Kotlin compiler won’t allow us to exit from the enclosing function using a return
inside it. This type of return is called a non-local return.
We can use non-local returns in inline functions because the lambda will be inlined in the call site. This return statement happens directly in the main
function, and not in the lambda function.
If we checked on the console output, only “Hello Foo” is visible. We use inline and let’s take a look at the decompiled Kotlin bytecode in Java source again.
public static final void main() {
String var1 = "Hello Foo";
System.out.println(var1);
}
By our knowledge previously related to inline function, those println('Hello Bar')
should also be inlined to the calling site in the main function. It happens because of non-local returns. It left all the code below that.
Now let’s edit one more time. What if we make foo pass the lambda function from the inlined context into a non-inline function?
We will get an error from the compiler that says Can’t inline ‘fx’ here: it may contain non-local returns. Add ‘crossinline’ modifier to parameter declaration ‘fx’
. Finally, we saw crossinline
keyword from the compiler. Let’s follow it by adding crossinline
modifier.
What happens next is, that the compiler rejects it, because there’s still a non-local return seen on the lambda function. Now we can conclude that crossinline
modifiers help us to prevent any non-local return that may affect the inline function.
Insight
Finally, let’s do the final example. Let’s use our first example related to HOF. We add our twice
function to be an inline function, and we do not want to add noinline
on the parameter it receives, instead we use crossinline
.
What would it be on the compiled bytecode in Java source? You can add your thoughts and answers in the comment section.