From Scala 2.13 to 3: Questions about inlining

I am trying to migrate a project from Scala 2.13 to Scala 3. It is going well and tests are already passing.

In this project I have two builds:

  • dev is without inlining because inlining breaks incremental building.
  • prod is with inlining because inlining yields a 5% performance gain.

Now I wonder how to migrate inlining. It seems that I have to replace all @inline annotations with just inline. But how to control inlining? As the Scala 3 compiler ignores the options -opt:l:method, -opt:l:inline, and -opt-inline-from, I see no way to disable inlining and to define from which modules to inline. (I think that the advice given by Scala Inliner and Optimizer | Lightbend regarding binary compatibility issues is still valid.)

Scala 3’s inline works differently from the @inline annotation in Scala 2.x, as it guarantees inlining, instead of being best effort.
This also means that disabling it could cause semantic differences. For example, it can be used to create custom compile time checks, e.g. you can check a constant for some condition with an inlined if, and throw a compile error with scala.compiletime.error. If that form of inlining was optional, you would get an error at runtime instead of compiletime.

Replacing all @inline annotations with inline will also probably bring you compile errors, as non-inlineable instances are then a compile error instead of being ignored.

I don’t know, if there is a replacement for the optimizer (the compiler options page lists -opt as unsupported). But I’d also check, if incremental compilation is still an issue at all, because the new inline is handled in a different compilation phase.
Regarding binary compatibility, I think the problem with the old inlining is, that it will also try inlining for certain methods that are not annotated with @inline (see the last bullet point on the Lightbend site you mentioned). If a library uses inline methods, they should be aware of not being binary compatible when changing them.

2 Likes

@crater2150, thanks for pointing out the differences between @inline and inline, although you mentioning compile time checks immediately brought back bad memories involving #ifndef NDEBUG and such things!

Yesterday, I gave it a try and replaced @inline by inline. The resulting code did not compile because of various issues at call sites, mainly type errors which did not occur without inlining, so I massaged the call sites until the build passed and the binary worked. (I had to remove one inline because it caused wrong byte code.)

Well, you probably can build ifdef-style things with inline, but it won’t be based on simply removing part of the sourcecode without syntax checks :wink:

A nicer use case (which sadly will only work when 3.0.2 is released) is using it to check literals for opaque types, e.g. to have an int type with a limited range, where creating one from a compiletime constant doesn’t need to be checked at runtime. See "Given" style Implicit Conversion with inline function for a discussion on this.

@crater2150, regarding incremental compilation of inline methods, I found out that it is currently broken: https://github.com/lampepfl/dotty/issues/11861

Oh, as the Lightbend article said “it breaks incremental compilation, and it makes the compiler slower”, I assumed it made the compiler recompile too often. Incremental compilation resulting in valid but outdated code is worse…