Java's record is faster, can Scala leverage it?

trust_final_non_static_fields

Which means Java’s Record is faster, and can scala’s case class leverage that?

I’m sure scala case classes have been used with reflection and mutated, which immediately disqualifies them. Scala case classes are ultimately assumed to be regular classes, so they behave as such.

Also, this is one JIT (albeit important). AFAIK graal doesn’t have this problem.

A benchmark (on all relevant platforms, e.g. JVM, Scala Native, Graal Native) comparing Scala’s case classes to Java’s records would be interesting nevertheless.

@MateuszKowalewski There is a benchmark in that blog and you can see the jdk’s cpp code too.

This might be the case but is also an extremely bad misuse of case class’es (as well as case class’es being non final by default). I mean you can do these things if you want to, but you will break properties of case class’es that people expect, i.e. with the final example, if you have a non final case class you can extend it and override methods like equals which will break unapply.

Honestly if people are mutating a case class by using reflection then you really should not be using a case class and instead just a regular class. Also do note that reflection is removed in Scala 3 (and for good reason).

At least personally, I would like to see stronger enforcement on the expectations for case class. Whether the appropriate way to do it in a way that its tied in with Java’s new records (so we kill two birds with one stone) is hard to say. It would basically mean that with JDK 21 for Scala 3 (maybe also Scala 2)? there is a new encoding if you happen to compile source code with JDK 21 but this can also be a pretty bad idea.

3 Likes

While I agree that modern case class idiom says not to do those things, the reality is that there’s plenty of code in the wild that does so; it’s not unusual for me to need to slap people on the wrist for using case classes in non-case-class ways.

(Especially in Scala 2, which it’s distressingly common for people to use case classes solely because they want to avoid typing new, but in practice they are treating them like ordinary classes.)

So yes, in principle this would be nice. But it would be a major source-code-level breakage.

Of course, which is why it needs to be done properly. However I think its important to raise the remark that its a good idea to do this at some point, we just need to do it carefully.

2 Likes

But we can make final case class works like Java record, or we can even get inline case class.

6 Likes

We could have something like a @record annotation that would ensure the case class is encodable (and ends up encoded) as a record in the JVM bytecode. Kotlin does something similar already (@JvmRecord).

Using Java records in Kotlin | Kotlin Documentation

5 Likes

I know backward compatibility, but something like @record should be imho the implicit default, and one could have some annotation to opt-out of the record encoding instead. Would be much better, imho!

It’s already super annoying that one needs to mark case classes explicitly as final… (Why wasn’t this actually repaired with the introduction of open classes? Or was it and I’ve missed it?)

AFAIK making case classes records in the bytecode is an implementation detail. What about compiling to records case classes with immutable fields and keep the class implementation for case classes with mutable fields?

So this:

case class Foo(a: Int, b: String)

would be compiled to

record Foo(int a, String b) {}

And this:

case class Wtf(var what: String)

becomes the usual:

class Wtf {
  //Default case class implementation
}

However, I am still worried about two concerns:

  • How this would interact with Scala-into-Java compatibility?
  • Does the compiler knows the language level it compiles to? Records are a Java 14+ feature.

Does the compiler knows the language level it compiles to?

I think it should, via the -release flag.

Yes, otherwise, Scala will be slower than Kotlin

also discussed previously Scala Pattern Matching on Java Records

I wouldn’t worry too much about this. The performance difference is small, even for this worst-case micro benchmark.

It also doesn’t apply to Graal VM.

Finally, you can change the system-wide setting with

  -XX:+UnlockExperimentalVMOptions
  -XX:+TrustFinalNonStaticFields

And I expect OpenJDK to eventually make this the default (or copy Azul’s Truly Final Optimization in Zing VM | by Anna Thomas | Azul Systems | Medium )

3 Likes

Actually scratch that. It would be much better to have extends java.lang.Record. That’s how it’s done with Scala’s enums which want to behave like Java’s enums, they extends java.lang.Enum[Xyz]

https://docs.scala-lang.org/scala3/reference/enums/enums.html#compatibility-with-java-enums-1

3 Likes

It would be much better to have extends java.lang.Record That’s how it’s done with Scala’s enums

That sounds like a regular way to do it. Would you like to open a SIP-proposal for this regarding extends java.lang.Record plus rules for when it can apply and compiler error or else etc?
(This thread title could perhaps be converted to a pre-SIP then) @sideeffffect @hepin1989

1 Like

I just found the jdk.internal.vm.annotation.Stable annotation which can be annotated on fields.

but doesn’t that apply only to internal jdk stuff? jdk has more annotations like @ForceInline but i’ve never seen them in any third-party code.

i’ve looked into javadoc for @Stable and @ForceInline and found that snippet:

 * @implNote
 * This annotation only takes effect for fields of classes loaded by the boot
 * loader.  Annotations on fields of classes loaded outside of the boot loader
 * are ignored.
1 Like

i guess that would be the subject of Project Leyden (among many other optimizations)