Value classes (I will call them inline classes) are useful for optimization: they can let you avoid object allocations when not really needed.
They have many constraint, however, they support implementation of Interfaces.
The trick that Kotlin uses to allow that can lead to bad performance.
Consider the following example:
interface MyInterface {
fun doStuff() = 1 * 2 * 3 * 4 * 5
}
inline class InlinedClass(val id: Int) : MyInterface
fun main() {
val worker = InlinedClass(42)
while (true) {
worker.doStuff()
}
}
Seems a very innocent code, right? But under the hood this is how it’s compiled to bytecode:
int worker = InlinedClass.constructor-impl(42);
while(true) {
InlinedClass.doStuff-impl(worker);
}
class InlinedClass {
public static int doStuff-impl(int $this) {
return MyInterface.DefaultImpls.doStuff(box-impl($this));
}
}
As you can see it is not more innocent: doStuff
(which is called continuously) now calls box-impl
, which is allocating an object.
If we remove the inline class modifier, only one object would be allocated throughout the whole execution, while, by making it inlined, we have a new allocation each round.
If we actually test the whole thing with the following code:
interface MyInterface {
fun doStuff() = 1 * 2 * 3 * 4 * 5
}
inline class InlinedClass(val id: Int) : MyInterface
class NonInlinedClass(val id: Int) : MyInterface
val rt = Runtime.getRuntime()
inline fun measureMemory(block: () -> Unit) {
val initial = rt.freeMemory()
block()
val final = rt.freeMemory()
println("Memory used: ${initial - final}")
}
fun main() {
val counter = 0..1_000_000
println("Inlined #1")
measureMemory {
val worker = InlinedClass(42)
for (i in counter) {
worker.doStuff()
}
}
println("NonInlined #1")
measureMemory {
val worker = NonInlinedClass(42)
for (i in counter) {
worker.doStuff()
}
}
// Repeat the test after warmup
println("Inlined #2")
measureMemory {
val worker = InlinedClass(42)
for (i in counter) {
worker.doStuff()
}
}
println("NonInlined #2")
measureMemory {
val worker = NonInlinedClass(42)
for (i in counter) {
worker.doStuff()
}
}
}
The results confirm that using an inlined class is increasing the memory consumption:
Inlined #1
Memory used: 6251584
NonInlined #1
Memory used: 0
Inlined #2
Memory used: 3145728
NonInlined #2
Memory used: 0
So the final point is: is really worth to allow inline classes to implements standard interfaces? What’s the benefit? When the class is used as interface, the object is always boxed and will behave just like a non-inlined class. The only advantage is that, after the method call (in the example doStuff
), the boxed element can be garbage collected, but the disadvantage is that you (kinda) lose control on where the boxing is happening and you may end up allocating multiple objects instead of only one.
Here’s another example where one may think that there is an improvement by using inline classes, but where there is actually zero (it’s always boxed):
interface Divisor {
fun divide(num: Int): Int
companion object {
fun fromInt(div: Int): Divisor = DivisorImpl(div)
}
}
private inline class DivisorImpl(val div: Int) : Divisor {
override fun divide(num: Int): Int = num / div
}
fun main() {
val divisor = Divisor.fromInt(10)
println(divisor.divide(100))
}