I’m unable to get even simple value classes to inline. Perhaps I’m missing something obvious, but I’ve tried several variations here, and even the following simple code doesn’t produce inlined results.
Code:
@JvmInline
value class Foo(val str: String)
fun test() {
val str = "hi"
val foo = Foo(str)
println(foo.str)
}
Bytecode (as shown by IntelliJ ‘Kotlin Bytecode’):
Why do you think it isn’t inlined? We don’t create an instance of Foo anywhere in this bytecode, we use the string directly. Foo.constructor-impl is needed in the case there would be any code in the constructor.
I just noticed that the return type of the constructor-impl is String It looks like there’s an intrinsic check being done in the pseudo-constructor, so there definitely is some code there.
My initial attempt was to use value classes in combination with inline functions. The hope was to have reusable code but preserve optimizations. In that case, I’m still seeing non-inlined class behavior. But it might just be too complex. I’ll try to create a streamlined example of what I’m looking for.
The class is getting inlined fully. No boxes are created. What you’re seeing is the machinery necessary to support having constructor checks in value classes. For instance, one could write:
value class NonBlankString(val string: String) {
init {
require(string.isNotBlank())
}
}
and the call to constructor-impl runs that check. This has barely any overhead, and any JIT or optimizer (like ProGuard) worth its money would absolutely inline that function call. The important thing is that no objects are created here, which is the point of value classes
The use case I was originally testing was something more akin to this:
interface SomeInterface {
val str: String
}
@JvmInline
value class Foo(override val str: String) : SomeInterface
inline fun <T : SomeInterface> doSomethingGeneric(getVal: (String) -> T) {
println(getVal("hi").str)
}
fun getFoo(str: String): Foo = Foo(str)
fun test() {
doSomethingGeneric(::getFoo)
println(getFoo("hi").str)
}
Decompiled (as the bytecode gets rather long):
public static final void test() {
int $i$f$doSomethingGeneric = 0;
String p0 = "hi";
int var2 = 0;
p0 = ((I)Foo.box-impl(getFoo(p0))).getStr();
System.out.println(p0);
String var3 = getFoo("hi");
System.out.println(var3);
}
In this case, the inlined code for doSomethingGeneric does not use the value class, whereas the manually-inlined code does.
Is this an issue with ordering, or is there some correctness concern here? I understand that a non-inlined call to doSomethingGeneric would have to use the boxed value, as it is being used in the place of a superclass. However, retaining that behavior upon inlining is slightly surprising.
My overall experiment was playing with Interface-based type definitions with KSP-generated value class implementations, backed by MemorySegments from the foreign memory API.
The consuming code would be made aware of the generated impls (where necessary), and therefore be able to take advantage of inlining to reduce memory churn.
If this isn’t possible, I can replace the interfaces w/ something more akin to a protobuf message definition (non-Kotlin), still generating the same code.
My guess would be the compiler simply doesn’t support this kind of optimization. As you said yourself: inline classes are boxed if used as a supertype. If using in an inline function we could technically avoid boxing, but this case would have to be targeted by Kotlin authors specifically. I don’t think “inlining a class” and “inlining a function” have too much in common in Kotlin - they are entirely separate features.
I’ve used this same pattern (inline-ish interfaces) a few times before, assuming the compiler would make this optimization too. It is disappointing to find out it’s not the case. I hope future improvements to K2 change this.