In this article I’ll take a look at creating Scala 3 inline
methods.
Background
First, from the Scala documentation: “inline
is a new soft modifier that guarantees that a definition will be inlined at the point of use.” This is different than the Scala 2 inline annotation, which notes, “An annotation for methods that the optimizer should inline ... If inlining is not possible, for example because the method is not final, an optimizer warning will be issued.”
Given that background, let’s see how the Scala 3 inline
keyword works on methods and functions.
Creating a Scala 3 inline method
About the simplest inline
example you can create looks like this:
class Writer(var prefix: String):
inline def print(s: String): Unit =
if (prefix != null) println(s"${prefix} ${s}")
In this example, the Writer
class has a print
method that I’ve defined to be an inline method. The main point of interest here is that this method checks to see if prefix
is null. If it’s null
, nothing happens, and if it’s not null
, the println
statement is run. That’s what you need to keep an eye on as we go through this exercise.
Creating a “point of use”
As the documentation states, this definition “will be inlined at the point of use,” so what we need now is a “point of use.” To create a point of use, I’ll create a little test App
:
object InlineTest extends App:
val w = Writer("Hello,")
w.print("world")
On the surface there’s nothing too exciting here, I just create a new Writer
instance and then call its print
method. Because the prefix
parameter is set, when I compile this code with dotc
and run it dotr
, it results in this output:
Hello, world
Nothing too exciting yet, but now let’s look behind the scenes.
Decompiling the class files
One way to see what’s going on here is to disassemble the Scala/JVM .class files with javap -c
. You can do that, but my degree is in aerospace engineering, not computer science, so I tend to prefer other tools.
You might be able to decompile the class files with IntelliJ IDEA, but I don’t know for sure, I didn’t test that. I initially decompiled the code with the Java Decompiler Project, and then ended up using JAD.
I’m amazed that JAD still works, and when I decompiled the InlineTest$.class file, I found this:
private InlineTest$()
{
Writer Writer_this;
MODULE$ = this;
App.$init$(this);
w = new Writer("Hello,");
Writer_this = w();
if(Writer_this.prefix() == null) goto _L2; else goto _L1
_L1:
Predef$.MODULE$.println((new StringBuilder()).append("").append(Writer_this.prefix()).append(" ").append("world").toString());
BoxedUnit.UNIT;
goto _L3
_L2:
BoxedUnit.UNIT;
_L3:
JVM INSTR pop ;
}
The key part of what you’re looking at is that all of the print
function code in the Writer
class ends up in the InlineTest
source code. Remember that “if null” test was in the print
method in Writer
class, and now this code is showing up in the InlineTest
class.
Similarly, if I create a second Writer
instance named w2
like this:
object InlineTest extends App:
val w = Writer("Hello,")
w.print("world")
val w2 = Writer("Hello,") //<-- NEW INSTANCE
w2.print("scala world")
and then go through the same process, I see this code in the JAD output:
private InlineTest$()
{
Writer Writer_this;
MODULE$ = this;
App.$init$(this);
w = new Writer("Hello,");
Writer_this = w();
if(Writer_this.prefix() == null) goto _L2; else goto _L1
_L1:
Predef$.MODULE$.println((new StringBuilder()).append("").append(Writer_this.prefix()).append(" ").append("world").toString());
BoxedUnit.UNIT;
goto _L3
_L2:
BoxedUnit.UNIT;
_L3:
JVM INSTR pop ;
Writer Writer_this;
// MY COMMENT: THIS IS THE CODE FOR THE SECOND INSTANCE
// ----------------------------------------------------
w2 = new Writer(null);
Writer_this = w2();
if(Writer_this.prefix() == null) goto _L5; else goto _L4
_L4:
Predef$.MODULE$.println((new StringBuilder()).append("").append(Writer_this.prefix()).append(" ").append("scala world").toString());
BoxedUnit.UNIT;
goto _L6
_L5:
BoxedUnit.UNIT;
_L6:
JVM INSTR pop ;
}
Lesson learned
So, lesson learned: Each time I use the print
method of the Writer
class, the logic from the print
method is copied wherever I call it. (Technically this is more like “lesson verified,” but it’s still cool to see how it works.)
The Writer##print method
It’s also important to note that when I decompile the Writer
class, I see that it has no print
method(!). As far as JAD can see, this is what the Writer
class looks like:
public class Writer {
public Writer(String prefix) {
this.prefix = prefix;
super();
}
public String prefix() {
return prefix;
}
public void prefix_$eq(String x$1) {
prefix = x$1;
}
private String prefix;
}
The print
method is not in this class — it’s been inlined everywhere it’s used.
The same steps without the inline keyword
In case you’re not comfortable with what this decompiled code normally looks like, let’s remove the inline
keyword from the print
method:
class Writer(var prefix: String):
// REMOVED `inline` HERE
def print(s: String): Unit =
if (prefix != null) println(s"${prefix} ${s}")
and then go back to our initial App
:
object InlineTest extends App:
val w = Writer("Hello,")
w.print("world")
Now when I compile the code with dotc
and then decompile the InlineTest$.class file with JAD, I see this output:
public final class InlineTest$ implements App, Serializable {
private final Writer w = new Writer("Hello,");
private InlineTest$()
{
App.$init$(this);
w().print("world");
}
// more code ...
}
That’s an amazing difference, isn’t it? This is what you expect to see with “regular” Scala code — i.e., non-inlined code — the InlineTest
class knows nothing about the internals of the Writer
class, it just executes its print
method.
Now if you decompile the Writer.class file you’ll see that its print
method is as you’d expect it to be:
public void print(String s)
{
if(prefix() != null)
Predef$.MODULE$.println((new StringBuilder()).append("").append(prefix()).append(" ").append(s).toString());
}
So there’s a huge difference between a “normal” method and an inlined method.
Summary
I’m fairly new to using inline
, but the obvious change here is that wherever the print
function is called, that call is eliminated and its code is inlined at that point.
Rather than trying to figure out the pros and cons of inlining methods, I google’d “benefits of inline methods,” and this C++ web page provides a list of the pros and cons. The pros of this technique (in C++) start with the elimination of the function call overhead, and then what duplicating that code at every point of use brings to the table. The cons are also related to the elimination of function calls and the duplication of code.
When I add the word “Scala” to that query I find that a user by the name of oxbow_lakes on this Stack Overflow page further states, “Never @inline
anything whose implementation might reasonably change and which is going to be a public part of a library.”
In general, everything I have read about inlining methods makes it sound like a performance technique, where you are trading code duplication for better performance. As with any performance-related technique, make sure you test your code to make sure you’re getting the performance improvement you expect. And as I learn more, I’ll update this article.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See also
Besides needing to look into this for my work on the 2nd Edition of the Scala Cookbook, I was inspired to look into this by Heiko Seeberger’s post about inline
. In that post he shows a more real-world example of how this can be useful, by limiting how other long-running code can be run as soon as possible.
In addition to his blog post, the Dotty inline
documentation shows a logging-related example, and also provides many more details on how this works, including a discussion of “transparent inline” methods. See that documentation for many more details.
On a personal note, I don’t know if the Scala Cookbook V2 will have a recipe like this, but that’s one thing about writing a book like this: you look at hundreds of things and then have to figure out what should be in the book, and what doesn’t make it in the end.