1. Introduction

Since version 2.0, the Scala compiler has included a JVM bytecode optimizer to produce bytecode that the JVM can execute faster. This optimization is in addition to all the optimizations already performed by the JVM itself. By default, it’s disabled, as it makes compilation slower.

One of the operations carried out by the Scala optimizer is inlining. Inlining is an optimization that replaces a function callsite with the body of the callee, typically for the sake of efficiency.

In this tutorial, we’ll first look at how to enable inlining and then at how to use @inline and @noinline in Scala.

2. Inlining in Scala

The main goal of the inliner in the Scala optimizer is to avoid megamorphic callsites.

A callsite is the line of code where we call a function:

def execute(fun: Int => Int)(n: Int): Int = {
  fun(n) // callsite
}

Megamorphic callsites are lines of code that can invoke three or more functions. This happens, for example, when we pass a function as a parameter to another function that then invokes it. For instance, the function execute above can run three or more functions:

val f1 = {n: Int => n * n}
val f2 = {n: Int => n + n}
val f3 = {n: Int => n}

execute(f1)(5)
execute(f2)(5)
execute(f3)(5)

In the example above, we’re passing three different functions, f1, f2, and f3 to execute. Therefore it’s a megamorphic callsite.

3. @inline and @noinline

@inline and @noinline in Scala are two annotations that control how the Scala optimizer inlines function callsites. In order to enable them, we must compile with the -opt:l:inline flag.

Once we enable the inliner, the @inline annotation tells the compiler to always try to inline the annotated method or callsite. Without it, the inliner applies some heuristics to identify the methods to inline. In this article, we’re not looking at how these heuristics work but instead focus on how inlining works when we use the annotations above.

@noinline, on the other hand, tells the inliner to never apply inlining to a given callsite.

Inlining in Scala is possible only if we declare the target function as final. Otherwise, the optimizer will never inline it, as subclasses might override the annotation.

3.1. @inline and @noinline on Functions Definition

Let’s see how to use those annotations when defining functions:

@inline
final def informalGreeting(name: String): String = s"Hi, $name"

@noinline
final def formalGreeting(name: String): String = s"Hello, $name"

final def veryInformalGreeting(name: String): String = s"Hey, $name"

The example above defines three final functions, informalGreeting, formalGreeting, and veryInformalGreeting. We annotate the first one with @inline. This means that the inliner will inline the call whenever possible. On the other hand, the inliner will never inline the second function. Lastly, it might inline the third function, depending on the heuristics.

Let’s see that with actual function calls:

def greetBaeldung1 = informalGreeting("Baeldung")
def greetBaeldung2 = formalGreeting("Baeldung")
def greetBaeldung3 = veryInformalGreeting("Baeldung")

greetBaeldung1 will be inlined if possible. For example, if we didn’t declare informalGreeting as final, inlining wouldn’t be possible. greetBaeldung2 is never inlined. greetBaeldung3 might be inlined, depending on the heuristics. Again, if veryInformalGreeting wasn’t declared final, inlining wouldn’t be possible.

3.2. @inline and @noinline on Functions Calls

We can override the @inline and @noinline annotations when calling a function. In other words, when we invoke a function, we can tell the inliner whether we want it to inline the call or not. Let’s see how this works:

def greetBaeldung4 = informalGreeting("Baeldung"): @noinline
def greetBaeldung5 = formalGreeting("Baeldung"): @inline
def greetBaeldung6 = veryInformalGreeting("Baeldung"): @inline
def greetBaeldung7 = veryInformalGreeting("Baeldung"): @noinline

In the example above, greetBaeldung4 overrides the @inline annotation of informalGreeting. Hence, the inliner will never inline the call. Similarly, greetBaeldung7 tells the compiler not to inline the call of veryInformalGreeting. On the other hand, greetBaeldung5 and greetBaeldung6 tell the optimizer to inline the call. Again, the compiler will inline only if possible.

Lastly, when annotating a callsite in a larger expression, we must use parenthesis to tell the inliner which part of the expression to inline:

def greetBaeldung8 = informalGreeting("Baeldung") + informalGreeting("Baeldung"): @noinline
def greetBaeldung9 = informalGreeting("Baeldung") + (informalGreeting("Baeldung"): @noinline)

In this case, greetBaeldung8 is equivalent to (informalGreeting(“Baeldung”) + informalGreeting(“Baeldung”)): @noinline, that is, the inliner will apply the optimization to the entire expression. If we want to narrow it down, we must do as in greetBaeldung9, where the inliner only optimizes the second call to informalGreeting(“Baeldung”).

3.3. Inliner Warnings

The inliner can issue warnings when it can not inline a callsite. Similar to deprecation warnings, the compiler shows the warnings at the end of the compilation. We can enable them by using the -opt-warnings flag.

Let’s see how these warnings would look like in practice:

class InliningWarning {
  @inline
  def f = 10

  def callsite = f
}

InlineWarning.f can not be inlined as it’s not final. Let’s compile it without -opt-warnings first and see what happens:

$> scalac InliningWarning.scala -opt:l:inline 
warning: there was one inliner warning; re-run enabling -opt-warnings for details, or try -help one warning found

Now, let’s add the -opt-warnings flag:

$> scalac InliningWarning.scala -opt:l:inline -opt-warnings
InliningWarning.scala:3: warning: InliningWarning::f()I is annotated @inline but could not be inlined:
The method is not final and may be overridden.
  def callsite = f
                 ^
one warning found

As expected, the warning tells us that the inliner could not optimize the call to f, as a subclass might override it.

4. Conclusion

In this article, we saw how inlining works in Scala. We took a theoretical look at the optimizer and at the problem of megamorphic callsites. Then, we dove into the usage of @inline and @noinline in Scala and analyzed their behavior both when defining and calling a function. Lastly, we saw how inlining warnings look like.

As usual, the code is available over on GitHub.


» 下一篇: ZIO简介