Surprisingly Good Benchmark Result With Value Classes and a Universal Trait

I have a benchmark result I find somewhat astonishing from a quick test with value classes and a universal trait.

I first wrote a bit of code with a sensor-feedback loop based on Ints. I then refactored that code to use value classes (extends AnyVal) to replace the Ints. I saw a 1% improvement; that’s the “no change” I’d hoped for. Terrific.

Then I replaced the “get the Int back out, do something with it, and put it back into a value class” with operations on a universal trait - which does the “get the Int back out, do something with it, and put it back into a value class” part inside its methods. That gave me a 20% improvement. (!!!)

I did not expect to see any difference at all, maybe a little worse. Not 4/5th’s the time.

I know benchmarking is hard and has to be done carefully, so we don’t need to go over that bit. If I’d detected some problem to investigate then I’d want to dig deeper but the result is much better than when I started, even though I would happily investigate and maybe tolerate a slow-down.

I just want some confirmation that this good result is not completely unreasonable.

Thanks,

David

Do you mean you refactored something equivalent to

class Foo(val a: Int) extends AnyVal { def foo = new Foo(a * 2) }

def bench(f: Foo) = f.foo

into

trait Bar extends Any { def a: Int; def foo = new Foo(a * 2) }
class Foo(val a: Int) extends AnyVal with Bar

def bench(f: Foo) = f.foo

?

In that case I would still expect performance degradation. Foo would have to be boxed every time foo is called. So if you see a performance improvement that’s probably due to some JIT magic and not due to scalac.

But value classes have a lot of traps that you can very easily fall into. E.g. they’re also boxed when you put them in a collection or any other generic context, and then unboxed when you take them out. So perhaps another possibility is that your value class is moving in and out of generic contexts a lot which causes a lot of boxing and unboxing. If instead you program against the universal trait everywhere (i.e. have List[Bar] instead of List[Foo]), your values will be boxed once but not boxed-unboxed-boxed the whole time, which could possibly cause a performance improvement…

1 Like

Thanks Jasper. That’s the sort of thing I’m looking for.

I have a create() method that’s just like foo(), which really should box.

I don’t ever put things in a collection, but do use a Tuple2 at one point, but it’s definitely using the value class, not the trait. Maybe java figured out my value class could be final from the universal trait? I guess I’ll take the luck of the JVM on this one.

1 Like

A Tuple2 is specialized for Int if I’m not mistaken. But I don’t know how specialization interacts with value classes.

1 Like