This code compiles with a warning (insignificant performance impact):
inline fun test(noinline f: () -> Unit) {
thread(
To the first and second question
How come (2) does not compile but (4) does?.. difference between
noinline
andcrossinline
2. inline fun test(crossinline f: () -> Unit) {
thread(block = f)
}
4. inline fun test(crossinline f: () -> Unit) {
thread { f() }
}
Both cases have inline
modifier instructing to inline both the function test
and its argument lambda f
. From kotlin reference:
The inline modifier affects both the function itself and the lambdas passed to it: all of those will be inlined into the call site.
So the compiler is instructed to place the code (inline) instead of constructing and invoking a function object for f
. crossinline
modifier is only for inlined things: it just says that the passed lambda (in f
parameter) should not have non-local returns (which "normal" inlined lambdas may have). crossinline
can be thought of as something like this (instruction to the compiler ): “ do inline but there is a restriction that it is crossing the invoker context and so make sure the lambda does not have non-local returns.
On a side note, thread
seems like a conceptually illustrative example for crossinline
because obviously returning from some code (passed in f
) later on a different thread cannot possibly affect the return from test
, which continues to execute on the caller thread independently from what it spawned (f
goes on to execute independently)..
In case #4, there is a lambda (curly braces) invoking f()
. In case #2, f
is passed directly as an argument to thread
So in #4, call f()
can be inlined and the compiler can guarantee there is no non-local return. To elaborate, the compiler would replace f()
with its definition and that code is then “wrapped” inside the enclosing lambda, in other words, { //code for f() }
is sort of another (wrapper) lambda and it itself is further passed as a function object reference (to thread
).
In case #2, the compiler error simply says it cannot inline f
because it is being passed as a reference into an “unknown” (non-inlined) place. crossinline
becomes out of place and irrelevant in this case because it could be applied only if f
were inlined.
To sum up, case 2 and 4 are not the same by comparing to the example from the kotlin reference (see "Higher-Order Functions and Lambdas"): below invocations are equivalent, where curly braces (lambda expression) "replace" the wrapper function toBeSynchronized
//want to pass `sharedResource.operation()` to lock body
fun <T> lock(lock: Lock, body: () -> T): T {...}
//pass a function
fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)
//or pass a lambda expression
val result = lock(lock, { sharedResource.operation() })
Case #2 and #4 in the question are not equivalent because there is no "wrapper" invoking f
in #2
Let me try to explain this by example: I'll go through each of your examples and describe what it orders the compiler to do. First, here's some code that uses your function:
fun main(args: Array<String>) {
test {
println("start")
println("stop")
}
}
Now let's go through your variants. I'll call the functions from your examples test1
..test4
and I'll show in pseudocode what the above main
function would compile into.
noinline
, block = f
inline fun test1(noinline f: () -> Unit) {
thread(block = f)
}
fun compiledMain1() {
val myBlock = {
println("start")
println("stop")
}
thread(block = myBlock)
}
First, note there's no evidence of inline fun test1
even existing. Inline functions aren't really "called": it's as if the code of test1
was written inside main()
. On the other hand, the noinline
lambda parameter behaves same as without inlining: you create a lambda object and pass it to the thread
function.
crossinline
, block = f
inline fun test2(crossinline f: () -> Unit) {
thread(block = f)
}
fun compiledMain2() {
thread(block =
println("start")
println("stop")
)
}
I hope I managed to conjure what happens here: you requested to copy-paste the code of the block into a place that expects a value. It's just syntactic garbage. Reason: with or without crossinline
you request that the block be copy-pasted into the place where it's used. This modifier just restricts what you can write inside the block (no return
s etc.)
noinline
, { f() }
inline fun test3(noinline f: () -> Unit) {
thread { f() }
}
fun compiledMain3() {
val myBlock = {
println("start")
println("stop")
}
thread { myBlock() }
}
We're back to noinline
here so things are straightforward again. You create a regular lambda object myBlock
, then you create another regular lambda object that delegates to it: { myBlock() }
, then you pass this to thread()
.
crossinline
, { f() }
inline fun test4(crossinline f: () -> Unit) {
thread { f() }
}
fun compiledMain4() {
thread {
println("start")
println("stop")
}
}
Finally this example demonstrates what crossinline
is for. The code of test4
is inlined into main
, the code of the block is inlined into the place where it's used. But, since it's used inside the definition of a regular lambda object, it can't contain non-local control flow.
The Kotlin team wants you to use the inlining feature sensibly. With inlining the size of the compiled code can explode dramatically and even hit the JVM limits of up to 64K bytecode instructions per method. The main use case is higher-order functions that avoid the cost of creating an actual lambda object, only to discard it right after a single function call which happens right away.
Whenever you declare an inline fun
without any inline lambdas, inlining itself has lost its purpose. The compiler warns you about it.
Q1: How come (2) does not compile but (4) does?
From their doc:
Inlinable lambdas can only be called inside the inline functions or passed as inlinable arguments...
Answer:
The method thread(...)
is not an inline
method so you won't be able to pass f
as an argument.
Q2: What exactly is the difference between noinline and crossinline?
Answer:
noinline
will prevent the inlining of lambdas. This becomes useful when you have multiple lambda arguments and you want only some of the lambdas passed to an inline function to be inlined.
crossinline
is used to mark lambdas that mustn't allow non-local returns, especially when such lambda is passed to another execution context. In other words, you won't be able to do a use a return
in such lambdas. Using your example:
inline fun test(crossinline f: () -> Unit) {
thread { f() }
}
//another method in the class
fun foo() {
test{
//Error! return is not allowed here.
return
}
}
Q3: If (3) does not generates a no performance improvements, why would (4) do?
Answer:
That is because the only lambda you have in (3) has been marked with noinline
which means you'll have the overhead cost of creating the Function
object to house the body of your lamda. For (4) the lambda is still inlined (performance improvement) only that it won't allow non-local returns.
From the inline functions reference:
Note that some inline functions may call the lambdas passed to them as parameters not directly from the function body, but from another execution context, such as a local object or a nested function. In such cases, non-local control flow is also not allowed in the lambdas. To indicate that, the lambda parameter needs to be marked with the crossinline modifier
Hence, example 2. doesn't compile, since crossinline
enforces only local control flow, and the expression block = f
violates that. Example 1 compiles, since noinline
doesn't require such behavior (obviously, since it's an ordinary function parameter).
Examples 1 and 3 do not generate any performance improvements, since the only lambda parameter is marked noinline
, rendering the inline
modifier of the function useless and redundant - the compiler would like to inline something, but everything that could be has been marked not to be inlined.
Consider two functions, A and B
inline fun test(noinline f: () -> Unit) {
thread { f() }
}
fun test(f: () -> Unit) {
thread { f() }
}
Function A behaves like function B in the sense that the parameter f
will not be inlined (the B function doesn't inline the body of test
whereas in the A function, the body: thread { f() }
still gets inlined).
Now, this is not true in the example 4, since the crossinline f: () -> Unit
parameter can be inlined, it just cannot violate the aforementioned non-local control flow rule (like assigning new value to a global variable). And if it can be inlined, the compiler assumes performance improvements and does not warn like in the example 3.