Noinline va crossinline. Can you clarify?

Hello! I am trying to understand a difference between noinline and crossnline keywords in use.

I know that noinline destroys the optimization given by the online keyword.
I noticed that I can use crossinline everywhere I use noinline.

What can you say about the crossinline in terms of optimization?

Thank you

Not a full answer but I just wanted to highlight that inlining is often more about specialized returns and reified generics than performance.

I’d be hesitant to set any function as inline for performance unless there’s a clear and evident need.

I’m sure someone else will have a better full answer for crossinline :slight_smile:

3 Likes

You can read my comment here: Coroutines and crossinline - #2 by broot It was about suspending, but I provided examples on inline, crossinline and noinline.

Are you sure this is not about performance?
There are many articles about the online keyword, which say that, inline reduce runtime overhead (lambda turns to a separate object and this is bad for the performance)
This one, for instance:

Thanks for you answer in that tread.

if I understand correctly:

  • noinline removes lambda inlining completely.

  • crossinline works in “automatic mode” - if this word is written, then it tries to leave inline optimization, if possible. If this is not possible, then it will work like noinline.

Right?

From my understanding, marking something as noinline will completely remove the inlininig, and create a separate function object for the lambda, decreasing the performance.

crossinline will still inline the lambda, but simply disallow returns.

Take this example:

inline fun test(block: () -> Unit) {
  print("Start of test")
  block()
  print("End of test")
}

fun main() {
  print("Start of main")
  test {
    print("Test!")
  }
  print("End of main")
}

As we did not declare the block parameter in the test function as crossinline, we can simply put a return@main after the print("Test!") statement, to return from the main function. The generated code will then look like this:

public static final void main() {
    String var0 = "Start of main";
    System.out.print(var0);
    int $i$f$test = false;
    String var1 = "Start of test";
    System.out.print(var1);
    int var2 = false;
    String var3 = "Test!";
    System.out.print(var3);
}

As you can see, we don’t even reach the statements after the return. If we instead mark the block parameter as crossinline, we can no-longer return the main function from inside the lambda. The compiler will tell you, that return@main is not allowed there. You can merely return@test, to jump out of the lambda and back to the normal function again. This time, all the remaining print statements will still be invoked. The generated code for crossinline will look like this:

public static final void main() {
    String var0 = "Start of main";
    System.out.print(var0);
    int $i$f$test = false;
    String var1 = "Start of test";
    System.out.print(var1);
    int var2 = false;
    String var3 = "Test!";
    System.out.print(var3);
    var1 = "End of test";
    System.out.print(var1);
    var0 = "End of main";
    System.out.print(var0);
}

TL;DR: crossinline will still fully inline the lambda, but simply prevent returns to the calling function from inside the lambda. So in terms of optimization, they work pretty much the same.

Or in other words, crossinline will give the optimization of inlining, without allowing the lambda to return the outer function directly.

If you want to mess around with the code the Kotlin generates yourself, you can simply create a Kotlin file in a project, then go to Tools → Kotlin → Show Kotlin Bytecode and then click Decompile.

Yes, I think we can say that.

inline means it is guaranteed the code can be fully inlined. Such guarantee is a requrement in order to use reified, non-local returns, etc. noinline means we don’t want to inline the lambda at all. crossinline means we would like to inline if possible, but we can’t guarantee we can do it or we can only inline it partially. For this reason we have to disable additional features which require full inlining, even if in some cases it would be possible to use them. crossinline could happen in situations like:

  • We can only inline partially, so we can’t inline it directly to the body of the caller, but we would still like to inline it in another place, for example in lambdas created inside the higher-order function (see my linked example).
  • There is a runtime condition and depending on it we either invoke the lambda directly or pass it somewhere else.
  • For now it is possible to fully inline, but we expect in the future it may be not possible, so we use crossinline already to avoid making backward-incompatible change at a later time.

You don’t see a difference between inline and crossinline, because your example is too simple. You didn’t try to pass block anywhere, which is the case when all 3 keywords start to do different things.

I see a difference between inline and crossinline, but both work roughly the same, in terms of them inlining the code to some site.

If you want to pass an inlined or cross-inlined parameter anywhere, e.g. into a variable, or as a lambda parameter to a non-inline function, it will fail, because that requires the lambda to be an object and not inlined. So you have to declare the block as noinline or remove inline from the function in that case. Take the following example:

fun someFunction(block: () -> Unit) {
  // Do something, this is not inlined.
}

inline fun test(crossinline block: () -> Unit) {
  // This will fail, even for crossinline, because block is still inlined,
  // but not an object itself.
  val x = block

  // This will also fail, because block is still inlined and cannot be passed as an object.
  // someFunction however requires its lambda parameter to be an object, because it is not inlined.
  someFunction(block)

  // This WILL work. And this is where the difference between crossinline and normal inline will show.
  // We don't pass block as a lambda parameter, but instead create a new lambda here.
  // While creating the lambda, we inline the contents of block into this.
  // The created lambda is now a new object, so someFunction can store it in a variable,
  // and also pass it to other no-inline functions.
  // Since we create a new lambda here, which has no guarantee about when it will be called,
  // let alone if it will even be called at all, we HAVE TO declare block as crossinline,
  // since whatever we write into block, CANNOT return to the outer function.
  someFunction {
    block()
  }
}

Let’s take the last example and let a function call test.

fun main() = test { println("Hello World!") }

test itself is still an inline function, despite its lambda parameter being crossinlined. The compiler will now generate the following:

public static final void main() {
  // The compiler will still inline the function test, as its an inlined function,
  // and thus directly call someFunction.
  // Since someFunction is NOT inline, the lambda HAS to be created as a separate object.
  someFunction(new MainKt$x$$inlined$test$1());
}

public final class MainKt$x$$inlined$test$1 extends Lambda implements Function0 {
  // ...
  public final void invoke() {
    int var1 = false;
    String var2 = "Hello World!";
    System.out.println(var2);
  }
}

As you can see, in this case crossinline will STILL inline the contents we pass in the lambda, in this case, by inlining them into the lambda we create. Due to this, we cannot allow returns to the outer function, since the control flow doesn’t necessarily go back to the call-site.

From the documentation:

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 of the inline function cannot use non-local returns, mark the lambda parameter with the crossinline modifier.

That being said, both lambdas with and without being marked as crossinline will generally get the optimizations of inlining, but serve different purposes. You mark lambas as crossinline if you store the lambda in a variable, pass it to another non-inlined function, or simply don’t want outer returns from the lambda.

If we would mark the block parameter of the test function as noinline, then we could directly pass the lambda to otherFunction. If we still create a separate lambda, then two lambda objects will be created, one containing the “Hello World!” and the other merely being a wrapper, that calls the first lambda.

1 Like

Yes, exactly, this is the case which I believe shows the difference between all 3 keywords the best. If we try to create a lambda that invokes block() and then pass this lambda somewhere:

inline fun foo(block: () -> Unit) {
    bar {
        // do something
        block()
        // do something
    }
}

fun bar(block: () -> Unit) { ... }

Then:

  • inline (no modifier) - doesn’t compile as inline requires the block to be inlined together with the code of foo.
  • crossinline - allows to inline block into the contents of the lambda, so only one lambda object is created. This is why the name “crossinline” - it doesn’t inline directly, but it inlines in further places (my own interpretation).
  • noinline - disallows inlining block into lambda, so we require 2 lambda objects here (although one of them is re-used between call sites).
1 Like

Performance is always a complicated topic in JVM, but function calls are generally considered to be pretty quick as long as the target is the same every time (so it is static, direct or monomorphic call). For this reason we don’t have to / shouldn’t make a function inlined just because we use it frequently. Kotlin compiler even generates a warning if using inline like this. I sometimes suppress the warning if creating functions like: inline fun isNotEmpty() = !isEmpty()

However, calls are much slower if the target often changes and this is a typical case for higher-order functions. We use the same higher-order function passing various lambdas to it, so it has to do dynamic lookups every time. And if it is a function like Iterable.map() then it has to call the lambda multiple times per a single higher-order call, so it is even worse. In these cases it makes sense to inline the function for performance reasons.

2 Likes

Thank you, now everything becomes clear.
Thanks for the decompiler advice also.

Broot, thank you too!

Are you sure this is good example:
inline fun isNotEmpty() = !isEmpty()
Is not a high-order function.

You mentioned the reified keyword. Can you explain why it should be used with inline together?

This is exactly my point. Generally, we should use inline only for higher-order functions or if using features that require inlining (reified, non-local returns) - in other cases Kotlin compiler assumes inlining is unnecessary and generates a warning. Despite this fact, I sometimes decide to still inline even if “unnecessary”, especially if I create very small utility functions like above. But frankly. I’m not sure if it makes any sense or not and I don’t suggest others to do the same.

I don’t want to go into deep details, but for technical reasons reification is possible only if the function is inlined.

Thank you