-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[wip] introduce option to consider scala companion objects stable #637
Conversation
We use the Oracle Contributor Agreement to make the copyright of contributions clear. We don't have a record of you having signed this yet, based on your email address fbrasil -(at)- twitter -(dot)- com. You can sign it at that link. If you think you've already signed it, please comment below and we'll check. |
Twitter has already signed the OCA, but I've also submitted the form by email. |
|
I can understand the motivation for such a change. However, we've avoided this kind of thing so far as it is basically adding intrinsics for something that Graal cannot prove came from a trusted source (unlike intrinsics for JDK classes). That said, putting it behind an option (whose default is BTW, I know it's only available as of JDK 11, but maybe Dynamic Constants can solve this scala use case? |
if (declaringClass.isInitialized() && field.getName().equals("MODULE$") && declaringClassName.endsWith("$")) { | ||
try { | ||
String companionInstanceClassName = declaringClassName.substring(0, declaringClassName.length() - 1); | ||
Class<?> companionInstanceClass = ClassLoader.getSystemClassLoader().loadClass(companionInstanceClassName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this code is correct. You really want the class loader of declaringClass
. This class loader is not exposed by JVMCI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can I get a hold of the correct class loader? Or is it not possible? I used the system class loader because it seems that's how other parts of the code base load classes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Class loaders are intentionally not part of JVMCI as JVMCI must also operate in environments where class loaders may not be supported (e.g. SVM and libgraal). What you want is the API introduced recently to resolve a String
to a JavaType
. Note however that this API is HotSpot specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is possible to keep the core mechanism language-agnostic and move the Scala-specific part into the configuration (i.e., flag value or flag configuration file)? |
@dougxc @thomaswue thank you for the reviews! I commented on the Scala issue mentioning the new Dynamic Variable feature. @thomaswue is your suggestion a more generic flag indicating that the application is in Scala? The optimization is already under a flag and disabled by default ( |
Yes, if we put this behind a flag, it has to be something generic that doesn't imply it is specific to Scala. I was thinking of something like |
The old encoding, with a static final field that was assigned in Our language semantics allow the uninitialized module field to be witnessed as null if it is referenced circularly during the call to the constructor of the superclass of the module. So I don't think that condy is suitable. The The I have a slight concern in using this compilation technique though because I've noticed that JDK 8 (which we still support) + C2 leaves some overhead, even after inlined |
As of JDK 9, there is the |
@retronym that's not the case for Graal, it does constant folding of companion objects in Scala 2.11 |
@retronym Could you clarify this statement? Do you mean that the heuristic is not enough? I imagine the field being witnessed as null wouldn't happen at the phase that the JIT kicks in. @dougxc @thomaswue this seems to have a considerable performance impact. I share of your concerns that this kind of rule is not ideal, but I wonder if this issue could affect the Graal adoption by the Scala community. We've been advocating Graal as an alternative to improve the performance of Scala applications, but if people don't see improvements using Scala 2.12 it's unlikely they'll adopt Graal. Note that Twitter is still on Scala 2.11, which is an exception. Most companies are already on Scala 2.12. |
I think an acceptable solution might be to define a
and provide a Scala specific implementation:
It means that instead of a Graal option, you would have to package
and on JDK9+:
The |
@dougxc That sounds like a good solution. The simplest and correct way to check for class C
object C
object D
@fwbrasil Here's a test case that shows that a call site could witness The field could also change if someone reflectively calls // class C { println(O) }
// object O extends C { println("O.init")}
class C {
C() {
Test.useO();
}
}
class O extends C {
/*stable*/ static O instance; // In Scala 2.11, this was also ACC_FINAL
static {
new O();
}
private O() {
super();
instance = this;
Test.useO();
}
}
public class Test {
static void useO() {
System.out.println(O.instance); // JIT shouldn't cache the null here
}
public static void main(String[] args) {
O o = O.instance;
}
} |
I suppose another solution, based on existing extension points, would be to use a javaagent to add the On JDK9+, this would likely require |
The javaagent suggestion is interesting although I assume it will impose quite a cost to startup (in addition to the problem of having to put app classes on the boot class path). With respect to your |
After speaking with @vjovanov and @axel22 , I can see why having to put extra things on a Scala command line would be a barrier for most Scala users. However, this has to be balanced against the risk of enabling a compiler optimization that can cause confusing behavior (e.g. someone uses reflection to re-assign a Speaking of performance, how much performance are we talking about? Are there benchmarks you can discuss? Would this optimization noticeably improve the performance of |
One general comment is that its a shame that there is no general purpose |
In the example, that method is called from within
Here's some benchmarking work: rorygraves/scalac_perf#32 (comment) Partially reproducing here: class MathTest {
@Benchmark def new2Min(bh: Blackhole) = newMath2.module.min(bh.i1, bh.i2)
@Benchmark def new3Min(bh: Blackhole) = newMath3.module.min(bh.i1, bh.i2)
}
final class newMath2 {
static newMath2 module;
static {
module = new newMath2();
}
int min (int i1, int i2) { return Math.min(i1, i2);}
}
final class newMath3 {
final static newMath3 module;
static {
module = new newMath3();
}
int min (int i1, int i2) { return Math.min(i1, i2);}
}
That benchmark shows the cost of wrapping
I think we'd need to try it out to see. I've been meaning to do this with |
The difference between the two versions is only in the null-check guard. I do agree that the microbenchmark itself is already a pain, since it makes it hard to write performance-critical algorithms in Scala. The only current way around it is to encode program constants as Scala macros (which is not a too bad solution for people that measure and care about performance, IMO). Otherwise, I think that the best solution would be to have a field annotation that allows saying Without the annotation, the stable field provider approach suggested by Doug seems like the best solution to me. |
I observe similar performance overhead in more complex scenarios. For instance: val mapF: Int => Int = _ + 1
@Benchmark
def map = Promise[Int]().map(mapF).map(mapF).map(mapF) # with ConsiderScalaCompanionObjectsStable=false
FutureBench.map thrpt 10 8857268.932 ± 1254596.248 ops/s
# with ConsiderScalaCompanionObjectsStable=true
FutureBench.map thrpt 10 9707714.600 ± 1001811.164 ops/s
# about 9% performance overhead I actually identified this issue while testing #636. The CAS virtualization can happen only if the offset is a constant. In Scala 2.11 it is, but in Scala 2.12 it's not since the constant folding can't fold the final offset field in the |
@fwbrasil , if the |
Quick question. @gilles-duboscq and I were looking at some companion object code, and we noticed bytecode like this getting emitted:
Why not publish the
or, if NPE are a problem, to publish the
If you do either, it seems like the |
Ok, we just saw Jason's comment in scala/scala3#1441 :
This emits this code:
Could
|
@dougxc I can work on it if we agree it's a good solution. I'm working on other scala-specific optimizations, so I wonder if we should create a separate project to bundle them and have releases for each Scala major version. Or should it be a Graal module? wdyt @christhalinger? |
I think the service provider proposal is a good technical solution but it might not be exactly what we, and especially the Oracle Labs folk, want. Twitter is advertising and advocating for using Scala with Graal for a while now because it works really well out-of-the-box. It would be a shame if that advantage is gone with Scala 2.12. And we all know that the first impression sticks for a very long time. A solution where you have to add a bunch of command line options and an additional module is not very user friendly. For us at Twitter it doesn't matter; we know what Graal can do for us and we wouldn't have a problem adding the additional options. But we should think about all the other Scala users we would like to use Graal... |
I'm with you @christhalinger - Graal should be the preferred choice for running Scala. That said, the solution originally proposed in this PR already requires an explicit command line option. We cannot allow Graal to deviate from Java semantics by default without some opt-in by the user (e.g., on the command line). And as you say, in environments where performance matters, extra command line options will not be a barrier. Maybe there is some other way to really solve this with pure Java? Not being a Scala language expert (or user!), I cannot assess how large the pure-Java solution space is or whether it has been fully exhausted. Maybe there are experts at Twitter or watching this thread who can chime in. |
BTW, the proposal for rewriting the initialization sequence will almost certainly not verify (you cannot directly call a super constructor on the result of a |
Yes, I absolutely agree with the "deviate from Java semantics" point. What do you mean by "pure Java" solution? Do you mean a change in the Scala language implementation? Also not a Scala expert (or user!) here ;-) Embarrassing... |
Yes, I'm hoping a change in the Scala language implementation might be possible. Something along the lines of what @axel22 proposed above but something that will also pass the bytecode verifier ;-) @gilles-duboscq had one possible idea but is not sure about its broader implications. Can you please outline that idea here Gilles. |
We have added a special case to the compiler to defer assignment to the For instance:
Is translated to:
We can extend this special case to object that don't assign to their ( I'm also pursuing the more general |
@retronym Here is a short summary of the solution that @gilles-duboscq came up with - objects that assign to their The singleton object fields (whether |
That sounds like it would work, and would avoid the need to break out to Java to define We'll have to get there gradually; it's too late to consider this change (which risks breaking reflective apps) or the In the interim, having a heuristic in Graal to deal with the current encoding would be welcome and worthwhile. |
We will continue to try improve Scala performance in general. Given that there now appears to be a scalac-based solution for good performance of Scala companion objects/classes, there's no need for the Scala specific switch proposed in this PR. Thanks for the useful discussion and feel free to suggest other Graal compiler heuristics that may be useful for Scala. |
@dougxc I'm not sure I understand. As @retronym mentioned, the language change might eventually happen, but it won't be implemented for the current major release (2.12), and won't make it to the next major release (2.13). Should we recommend people to avoid Graal for these releases? It's likely that companies will take years to be on a release > 2.13 |
Graal already has performance advantages for executing Scala without this specific change so I'm not sure it makes sense to recommend that Scala users avoid Graal. |
I agree with @fwbrasil and I think it's a mistake to close this bug. At VMM I talked with @thomaswue about this issue and it seemed he was very interested in solving this issue for the current Scala version. |
Hey @christhalinger , we're all on the same page with respect to wanting Scala companion objects to be fast on Graal. However, this specific PR contains a proposal to do it in a way that deviates from Java semantics based on a Scala specific option. If you have other proposals that don't have these drawbacks, I'd love to hear them ;-) |
Working on it... ;-) |
Work in progress
Code is functional, but still lacks unit tests.
Problem
In order to support Java 9, the Scala compiler developers had to make the companion object fields not final in Scala 2.12. See scala/scala-dev#194 and scala/scala3#1441.
This new encoding doesn't allow Graal to perform constant folding of companion objects and its final fields.
Solution
Introduce the option
ConsiderScalaCompanionObjectsStable
that enables an heuristic that determines if a field is a scala companion object and considers it as a stable value.Notes
Would you be comfortable with introducing this kind of language-specific heuristic?
Is it ok to use the system class loader here?