-
Notifications
You must be signed in to change notification settings - Fork 1.7k
virtualize unsafe compare and swap calls #636
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
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. |
|
LogicNode equalsNode = CompareNode.createCompareNode(EQ, expected, read, tool.getConstantReflectionProvider(), NodeView.DEFAULT); | ||
|
||
FixedGuardNode guardNode = new FixedGuardNode(equalsNode, DeoptimizationReason.Aliasing, DeoptimizationAction.InvalidateRecompile); | ||
guardNode.replaceAtPredecessor(predecessor()); |
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.
This node has just been created and has no predecessor, this will not do anything. If it did it would be a problem anyway since in virtualize
every graph modification has to be delayed using the tool
(as you have done below).
|
||
LogicNode equalsNode = CompareNode.createCompareNode(EQ, expected, read, tool.getConstantReflectionProvider(), NodeView.DEFAULT); | ||
|
||
FixedGuardNode guardNode = new FixedGuardNode(equalsNode, DeoptimizationReason.Aliasing, DeoptimizationAction.InvalidateRecompile); |
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.
A deopt without any alternative is a bit dangerous performance-wise: it might lead to deopt-loops. In particular here the "action" is InvalidateRecompile
which is quite costly considering that the recompilation will lead to exactly the same code.
What about just using ConditionalNodes
?
ConditionalNode fieldValue = ConditionalNode.create(equalsNode, newValue, read);
ConditionalNode result = ConditionalNode.create(equalsNode);
//...
tool.setVirtualEntry(obj, index, fieldValue);
//...
tool.replaceWith(result);
In the "simple" cases it will immediately fold away and in more complex cases where equality can't be statically proven, performance should still be OK.
|
||
// @formatter:off | ||
@Option(help = "Virtualize unsafe CAS calls", type = OptionType.Expert) | ||
public static final OptionKey<Boolean> VirtualCAS = new OptionKey<>(false); |
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.
This option is fine while you're experimenting but ultimately i don't think we need an option for this.
Thank you for the PR, that's a good idea. I've answered some of your questions in comments. Regarding the |
77d0b9d
to
23bb6cf
Compare
@gilles-duboscq thank you for the review! I've changed the impl to use |
23bb6cf
to
26e96ba
Compare
Right. I guess this is due to
Based on this it might be enough to just gate the In any case, i think we can remove the option and we should be good to go. |
@gilles-duboscq it seems more complicate than that. For some reason the partial escape analysis tries to remove twice the constant node representing true. I'm testing this new version with a service and it seems that there's a bug. The constant folding somehow infers the comparison as false in some cases where they should be true. I'm investigating if it could be related to the stamps of the values. |
I tried to write a few unit tests on top of your changes but i couldn't reproduce either error. |
@gilles-duboscq thank you for looking into it. I haven't been able to reproduce the issue with the logic constant being false in isolation yet. For the invalid graph issue, this always fail if I remove the if condition for logic constants: public static void main(String[] args) {
while (true) {
test();
}
}
private static boolean test() {
AtomicInteger a = new AtomicInteger(0);
return a.compareAndSet(0, 1);
}
|
it's a concurrency bug, not related to the stamps. I'm analyzing it |
@gilles-duboscq should we add a memory barrier somewhere? It seems that other threads see class instances set through a CAS operation with some of their fields null. I haven't been able isolate the issue, but you can reproduce it using:
If you use |
I had a more thorough look at this. You should use Regarding the other issue, i can not reproduce it when using
Also node that this kind of problems is usually detected earlier if you use |
26e96ba
to
1f93e5a
Compare
@gilles-duboscq thank you for the pointers! I've implemented the logic you suggested, but I couldn't find any CAS operations in our service where both values are virtual, or both non-virtual. They're usually virtual for the new value but a I've done some more testing, and the EDIT: I've tested another codebase and found one instance where both are non-virtual |
ff2df3e
to
37d889f
Compare
@gilles-duboscq I've added test, removed the option, and removed the virtual object comparison since it's covered by the constant fold applied when the equals node is created. It should be good to merge |
8c3f21a
to
3a68fe1
Compare
the build failure doesn't seem related to the change: https://travis-ci.org/oracle/graal/jobs/424908424#L1693 |
tool.replaceWith(ConstantNode.forBoolean(equals)); | ||
|
||
} else if (!(expectedAlias instanceof VirtualObjectNode) && !(newValueAlias instanceof VirtualObjectNode)) { | ||
ValueNode fieldValue = ConditionalNode.create(equalsNode, newValueAlias, currentValue, NodeView.DEFAULT); |
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.
Did you try
Object o = new Object();
AtomicReference<Object> a = new AtomicReference<>(o);
return a.compareAndSet(obj1, obj2);
?
If currentValue
is virtual but expected or new values are not? The created conditional node should lead to issues (virtual only on one side).
You should either disable optimization in this case or look at how ObjectEqualsNode#virtualize
resolves ==
in various cases (note that you might also be able to resolve the case where both currentValue
and expectedAlias
are virtual if you want).
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.
Just added a test for it: https://github.com/oracle/graal/pull/636/files#diff-4039dbafeadfd69113c08dcedbdd913cR114.
It takes the branch where both values are non-virtual and virtualizes the CAS. I'm also running this version in a large codebase without problems.
Looking at the ObjectEquals
virtualization, it'll infer that the the objects are not equal if one of them is virtual and the other is not, so it should be ok.
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.
If you return a.get()
rather than the result of the CAS, you'll see in the graph that it returns a ConditionalNode
with a Virtual
node on one side an a LoadField
on the other, although this test does not fail, we can not generate valid code for that, it would just crash later in the compilation process. See for example this test:
public static Object onlyInitialValueVirtualMatch2() {
AtomicReference<Object> a = new AtomicReference<>(new Object());
a.compareAndSet(obj1, obj2);
return a.get();
}
@Test
public void onlyInitialValueVirtualMatchTest2() {
testEscapeAnalysis("onlyInitialValueVirtualMatch2", null, true);
assertTrue(graph.getNodes(LogicCompareAndSwapNode.TYPE).isEmpty());
test("onlyInitialValueVirtualMatch2");
}
This last line with test
will try to actually compile and use the resulting code which fails because we don't generate code for virtual nodes.
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.
Thanks for the example. I've added a condition to check if the current value is not virtual as well.
3a68fe1
to
b8e9bb3
Compare
b8e9bb3
to
5f9bd84
Compare
|
||
@Test | ||
public void onlyInitialValueVirtualMatchTest() { | ||
testEscapeAnalysis("onlyInitialValueVirtualMatch", null, true); |
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.
Unfortunately, this test now fails, (testEscapeAnalysis
makes sure there is no more allocation in the snippet after escape analysis).
You can:
- just remove it
- handle more virtual vs. non-virtual cases by resolving the
==
likeObjectEqualsNode#virtualize
does (this case for example is virtual vs. non-virtual where both types have identity ->false
)
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've just removed the test
5f9bd84
to
e5cab6b
Compare
Thank you @fwbrasil for bearing with all my comments and making the adjustments! |
Hi @fwbrasil , have you had a chance to measure the performance impact of this change? |
Problem
Compare and swap calls using
Unsafe
are currently always lowered to an atomic operation. That's unnecessary when the CAS mutates a field of an instance that is virtual. It introduces some performance overhead and prevents other optimizations.Let's take this JMH benchmark class as an example:
The if/else version has much better performance than the version using the unsafe compare and swap:
It is optimized before lowering to a graph that returns a constant:
That's expected since the instance doesn't escape and constant folding can determine that the CAS will always return true. The same doesn't happen with the unsafe version since the compare and swap node is opaque to constant folding:
Solution
When an object is virtualized, compare and swap can be replaced by a guard and the new value can be set using the
VirtualizerTool
. I've implemented such optimization and the benchmark results are promising:With the CAS virtualization, the unsafe compare and swap benchmark is also optimized to a constant:
Notes and questions
I've used a guard that will trigger deoptimization in case the current value doesn't match the expected value. It assumes that users won't write code that will makes a CAS operation fail since the object doesn't escape and can't be used concurrently.
I couldn't link the guard node to the predecessor using the
VirtualizerTool
, so I set it inUnsafeCompareAndSwapNode.virtualize
. I'm not sure if that could be problematic since it seems that all effects should be done through the tool during virtualization.The optimization is applied only when the field can be resolved. If the user uses a dynamic or invalid offset, it won't be applied.
I've added an option to enable the optimization. Should it be enabled by default?