Consider indy-fy boilerplate around case class
#17864
Replies: 0 comments 20 replies
-
One problem with using invokedynamic is that it doesn't work for the JS and native backend, so we'd need to maintain two codepaths. |
Beta Was this translation helpful? Give feedback.
-
I recently learned that Scala supports macros that generate final case class SmallScalaModified(
i: Int,
s: String,
l: Long
) {
override def equals(other: Any): Boolean = Macros.equals(this, other) // generates an `invokedynamic`
} Bytecode before
Bytecode after
I made a handful of equality benchmark cases and here are the results (running on JDK 16): I was concerned that maybe these results would only work for recent JDKs (perhaps due to progress in optimizing Given how significant the speedup is and how much boilerplate bytecode it would remove, I think this would be worth maintaining multiple codepaths. |
Beta Was this translation helpful? Give feedback.
-
One further update: creating an OTOH, my initial attempts to indy-fy At this point, I don't think there is much more left to try. From my initial list:
|
Beta Was this translation helpful? Give feedback.
-
Very interesting benchmark results! /cc @retronym who should be interested in this.
That's good to know, it would also be interesting to try this on graalvm I think. |
Beta Was this translation helpful? Give feedback.
-
A benchmark variant that might be interesting: generate a bunch of class instances with random values, store them in an array, and compare their equality with java.util.Arrays.deepEquals. |
Beta Was this translation helpful? Give feedback.
-
I'll try this after work and post plots.
Interesting too... I'll try this (probably with something like |
Beta Was this translation helpful? Give feedback.
-
Ah yeah, I forgot about Arrays.equals |
Beta Was this translation helpful? Give feedback.
-
Here are the new tests. I'm getting more and more confident that this is a change worth implementing. Different JVMsAs a reference, here is HotSpot 16 again: I checked GraalVM 16 (from here). Unsurprisingly, looks quite similar to HotSpot (interestingly a little slower - the max y is different). OpenJ9 performs abysmally on things related to Some more checksThinking about the comment from @smarter, I realized comparing randomly generated records would not be very interesting: that'd be overwhelmingly testing the path that short-circuits to I was initially expecting this to be a plot with bars growing linearly from left to right (and they sort of do for the non- Still, to get a more even picture, I did one more benchmark where I would repeatedly loop through all the equality checks from the previous plot (so as to prevent the JIT from overfitting |
Beta Was this translation helpful? Give feedback.
-
Thanks for investigating this. I ran your benchmarks to reproduce the results and understand the performance gap. Looking that the JIT inlining logs (either with
The accessor methods for the String-typed fields aren't being inlined! After reading:
... I tried some of the workarounds. Both One thing I don't understand is why some method call to In practice you'd be pretty unlucky in real code not to have Could the same problem happen for user-classes? I think it is relatively unlikely, but not impossible. Here's one scenario:
Similar scenarios could appear if the equals method always had a fast path result of It is likely, though, that in JIT would have assumed that part of the method was dead and the new data shapes would deoptimize and recompile, after which the The equals implementations used by scala> case class A(s: String); class B extends A("") { override val s = "B.s" }; A("B.s") == new B
class A
class B
val res1: Boolean = true All that said, I do think it is worthwhile using this approach to reduce classfile size! |
Beta Was this translation helpful? Give feedback.
-
Thanks for trigging into the difference and sharing the process - this is very informative!
This is very reassuring and good to know. I'm going to rerun some of my other benchmarks while tracing the JITing that happens (ex: is the difference in
Good point, although this is actually a feature of
Is there anywhere it would be safe to start working on something like this, or should I come back to this after Scala 3.0 is released and settled? Adding |
Beta Was this translation helpful? Give feedback.
-
You can open a draft PR in the dotty repo and mark it as for 3.1. |
Beta Was this translation helpful? Give feedback.
-
I'd like to do this now. I think I should be able to figure out the bootstrap method and the bytecode generation, but I'm a bit stuck around what to do around the AST and dotty architecture. The methods whose implementation I would like to turn into a single @smarter do you have any advice/suggestions on the above? |
Beta Was this translation helpful? Give feedback.
-
Trees are tricky since they need to be copied/transformed. @sjrd can confirm but I think the usual way to have backend-specific behavior is to detect a particular code pattern in the backend itself, so perhaps you can just check for synthetic equals/hashCode methods in case classes |
Beta Was this translation helpful? Give feedback.
-
Adding new kinds of trees is problematic, because they have to be handled by every other phase that sits in-between. Detecting code patterns is possible, but unless it's based on very specific criteria (like recognizing that it is a case-class-generated Another possibility is to introduce primitive "magic" methods. Instead of generating special trees, you generate calls to those magic methods. Care must still be taken if their semantics cannot be expressed in regular Scala semantics, as intermediate phases may still break some assumptions. That seems to still be the most reliable technique, though. One example of such a magic method is |
Beta Was this translation helpful? Give feedback.
-
Something else to keep in mind is that pattern matching has some subtlety and doesn't always correspond to exactly one class Outer {
case class Inner(x: Int)
} The |
Beta Was this translation helpful? Give feedback.
-
@smarter I plan on diving deeper into making sure semantics are preserved, but only once I have a clearer picture on the how the compiler changes should work. @sjrd I followed your suggestion and have a prototype that replaces the
I'm still not sure what should be done for the JS backend. Is it a bad idea for As for getting more feedback and help, at what point should I open a draft PR? I also seem to remember Gitter being used, but https://gitter.im/lampepfl/dotty pointed me to https://gitter.im/scala/contributors which is a 404. Is there any chat channel where I can ask small dotty questions? |
Beta Was this translation helpful? Give feedback.
-
That can be done as early as you want.
We've been experimenting with https://github.com/lampepfl/dotty/discussions for that purpose. |
Beta Was this translation helpful? Give feedback.
-
There is a #scala-contributors channel on Discord (https://discord.com/invite/scala), but so far it hasn't been very common to see active Scala 3 maintainers answering questions there, so Guillaume's link is probably a better bet for specific technical questions. (You'll probably increase your hit rate if you mention that it's in the context of a PR you're working on, and not just a random free-floating question.) |
Beta Was this translation helpful? Give feedback.
-
We'd also need to handle the scala native backend which is trickier since it's just a plugin and therefore we don't have a built-in setting in the compiler to check if it's enabled, not sure how we should deal with that. |
Beta Was this translation helpful? Give feedback.
-
Maybe a flag to opt out of this (implied by SJS and manually enabled in the Native plugin)? I'm really not familiar with the Dotty compiler plugin interface... |
Beta Was this translation helpful? Give feedback.
-
Taking a page from Java's implementation of
record
, Scala could change the codgen for some auto-generated methods incase class
to justinvokedynamic
with a bootstrap method similar toObjectMethods
(that's what Java uses to implementtoString
/equals
/hashCode
onrecord
s). Since the specifics of Scala's equality, hash code, and string representation differ from Java records, we'd need to add somescala.runtime.ScalaCaseMethods
(instead of directly re-usingObjectMethods
).I think this trick would work for:
toString
hashCode
equals
productElementName
productElement
copy
(maybe - the current bytecode is already pretty trivial)unapply
apply
The motivation for this is that:
scala.runtime.ScalaCaseMethods
Of course, we would need to make sure this does not regress performance. I threw together the very simplest of benchmarks and results were very encouraging (although likely muddled due to the fact that Scala-generated
equals
has different semantics).Beta Was this translation helpful? Give feedback.
All reactions