-
Notifications
You must be signed in to change notification settings - Fork 6.2k
8295486: Inconsistent constant field values observed during compilation #11861
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
|
👋 Welcome back thartmann! A progress list of the required criteria for merging this PR into |
|
@TobiHartmann The following label will be automatically applied to this pull request:
When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command. |
Webrevs
|
chhagedorn
left a comment
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.
That looks reasonable, nice test!
|
@TobiHartmann This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be: You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 39 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. ➡️ To integrate this PR with the above commit message to the |
|
Thanks, Christian! |
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.
@vlivanov Do we have a mechanism to verify that Stable fields are initialized only once? Based on Tobias's test (which continuously modifies values) it does not work if it even exists. Then what is definition of Stable attribute then? We can't constant fold a field's value which can be changed (except from NULL to a value).
Tobias's comment in JBS about field point to different DirectMethodHandles concerns me.
For me the only valid solution is throw out compilation if it see change in stable filed's state. We have validation step in ciEnv::register_method(). May be create table of stable fields values we folded and verify during registration to make sure it did not change. It may not catch case in Tobias's test where the same values are rotated.
Note, one change from null to not-null is valid case. May be new code in PhaseCCP::verify_type() should check for initial NULL value before skipping following checks.
|
Unfortunately, there is no such mechanism. In the case of racy initializations, it can frequently happen that a stable field is written multiple times by different threads (see also my comments in JDK-8288970). This is expected and also mentioned in the C2 code: jdk/src/hotspot/share/opto/compile.hpp Line 266 in 775da84
The definition of @Stable is basically that we can constant-fold any value once it's initialized and (semantically) it does not make a difference which value we choose.
If we go with your proposed solution of a table of field values, we can as well use that one as cache instead of bailing out from compilation, right? |
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.
Nice test!, problem seems to be intermittent.
src/hotspot/share/opto/phaseX.cpp
Outdated
| assert(!told->isa_int() || !tnew->isa_int() || told->isa_int()->_widen <= tnew->isa_int()->_widen, "widen increases"); | ||
| assert(!told->isa_long() || !tnew->isa_long() || told->isa_long()->_widen <= tnew->isa_long()->_widen, "widen increases"); |
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.
Minor correction, told->isa_int()->_widen <= tnew->isa_int()->_widen can be replaced by told->is_int()->_widen <= tnew->is_int()->_widen, preceding expression makes sure types are ints
src/hotspot/share/opto/phaseX.cpp
Outdated
| if (FoldStableValues && n->is_Load() && | ||
| ((atp->field() != NULL && atp->field()->is_stable()) || | ||
| (adr_type->isa_aryptr() && adr_type->is_aryptr()->is_stable()))) { | ||
| return; |
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.
Okay. The spec is really bizarre for @stable
Right. I prefer caching because the value will be consistent at least during compilation and it may allow some optimizations to happen. Imaging you read same stable field on separate paths and then compare constants. |
|
I fully support caching of non-default values. (Not sure about default values yet: at least, it makes sense to cache them as well to make compilations reproducible.) As an idea, the table could be built on top of existing dependency mechanism, but |
As Tobias already pointed out, there's no such support in place yet. Only a long-standing RFE to add it: |
If value keep changing during CCP then it will delay the convergence. Thus a convergence will be reached only for a stable value, which is what caching will also achieve. Stable field value should be added to method dependencies such that compilation becomes invalid if stable value changes later on. Agree with @iwanowww suggestion |
It's too much for @stable. It's allowed to observe stale values when concurrent modifications happen. It would require dependency checking on every On the other hand, caching a single value which is used across the whole compilation has only performance impact (whether non-null value is observed or not). |
Agree, but there should be a way for method to deoptimize if stable field value changes past method registration. Only full proof solution is to enforce that stable field value does not change after single initialization apart from null to non-null transitions. |
|
Vladimirs, Jatin, thanks for the reviews and discussion! The question is what we want to achieve with this patch:
I think my proposed patch achieves 1) which is also the only issue we ever observed in real code: Racy initialization of method handles for indy string concat in core libraries code. The verification code proposed by (JDK-8024042) would catch this. Constant folding a stale value does not matter here, semantically, we can use any of the equivalent method handles that we observe. It just confuses CCP verification. Convergence of CCP is also not an issue because we don't update a node's type indefinitely but only if the type of a relevant input node changed. Regarding 2): Couldn't any such wrong/undesired behaviour happen with execution in the interpreter as well? The only difference would be that optimized C2 compiled code would always behave that way. But according to the specification for
Also, doesn't the above specification mean that all non-synchronized initializations of stable fields can lead to undefined behavior?
That will also happen with interpreted code when the stable field is written between the two reads. The only complete solution would be 3), which, as Vladimir I. already pointed out, does not seem feasible given that we would need to intercept all writes to stable fields and array elements. I'm not against caching but I'm wondering how much sense it makes to apply an expensive and complex (partial) fix to C2 while C1 and the interpreter are still affected. Shouldn't the remaining issues be fixed in Java code if undefined/unexpected behavior is ever observed? |
|
IMO we should focus solely on C2 where aforementioned inconsistencies trigger assertion failures and (rarely) break compilation replay. (The rest is classified as user errors according to |
It is not required for proper
|
|
FTR there have been multiple attempts to build a story for "lazy final" fields: |
+1 C2 generated code is "final" (C1 code and Interpreter are executed only at initial stage with exception of deoptimization). |
|
Okay, I implemented caching of stable values. Please let me know what you think. |
vnkozlov
left a comment
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.
Nice work.
src/hotspot/share/ci/ciEnv.hpp
Outdated
| ciConstant value() const { return _value; } | ||
| }; | ||
|
|
||
| GrowableArray<StableValue>* _stable_values; // Cache of stable values |
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.
Aren't ciInstance and ciArray provide a better place to cache field/element values (logic can be encapsulated in ciInstance::field_value() and ciArray::element_value())?
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.
And it would seamlessly cover all possible cases, like static final fields (which can be concurrently updated through Reflection API or Unsafe).
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.
But wouldn't that require duplication of the ciEnv::check_stable_value logic and having a GrowableArray per ciObject instance?
Regarding covering more cases like static final fields: I intentionally omitted them because I don't think it's possible to delay constant folding of such loads until CCP (since null is a valid value) and the lookup is done only once during (I)GVN. The regression test includes a corresponding case (see testFinal). Even with unsafe accesses, if the field holder is not known at parse time, the access is conservatively marked as mismatched and thus no constant folding is attempted later.
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.
But wouldn't that require duplication of the ciEnv::check_stable_value logic and having a GrowableArray per ciObject instance?
Are you primarily concerned about footprint? The field can be lazily initialized to reduce possible effects.
check_stable_value still can be shared (e.g., placed on ciObject).
Regarding covering more cases like static final fields: I intentionally omitted them because I don't think it's possible to delay constant folding of such loads until CCP
If you want to limit the current patch to stable case only, that's fine with me. But keep in mind that we still want to uniformly treat all cases where field/array element values are considered constant. Concurrent field modifications introduce non-determinism into compilation process which we would like to avoid. So, even if we don't cover final fields right away, let's prepare for a future enhancement which will cover that. In that respect, stable references in names are misleading.
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.
My concern is that putting the logic in ciObject adds a new field to all subclass instances while it's only rarely needed. Given that a large number of these objects are created during compilation, that adds to footprint even if the cache is initialized lazily.
I still gave this a try and another issue is that using ciConstant in ciObject creates a circular dependency (invalid use of incomplete type) because ciConstant depends on ciObject. We could of course duplicate the logic and put things both into ciInstance and ciArray but then a side table in ciEnv seems preferable to me.
Until proven otherwise, I would like to limit the current patch to stable cases because I don't think there are any issues for non-stable cases. I can rename the code from "stable" to "constant" but I'm not sure how much sense that makes given it's all limited to and guarded by FoldStableValues. What do you think?
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.
Personally, I'm not concerned about footprint increase from a field on ciObject class. Those represent oop constants observed by JITs and their count should be modest.
Speaking of relation between ciConstant and ciObject, ciConstant.hpp only refers to ciObject*, so the cycle can be broken by prepending class ciObject; in ciConstant.hpp.
I don't think there are any issues for non-stable cases.
I'm fine with addressing only stable cases with the current patch for now, but I don't agree there's nothing to improve for non-stable cases.
Concurrent field modifications may seem benign, but our previous experience shows that it's safer to work with a consistent view through CI (when it is feasible/possible).
(There's a relevant RFE filed: https://bugs.openjdk.org/browse/JDK-8294616) And it helps improve compilation replay along the way. And it's not specific to C2.
So, IMO in the longer term CI is the right place to fix it (irrespective whether stable or all constant cases are handled). It's a bit strange to see the new checks added in opto/type.cpp (irrespective of where the cache is located).
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.
Speaking of relation between ciConstant and ciObject, ciConstant.hpp only refers to ciObject*, so the cycle can be broken by prepending class ciObject; in ciConstant.hpp.
That won't help. All ci* classes, including ciObject, are already forward declared in ciClassList.hpp which is included in ciConstant.hpp. The following usages in ciConstant require a complete ciObject type:
jdk/src/hotspot/share/ci/ciConstant.hpp
Line 115 in c5c8c06
| return as_object()->is_null_object(); |
jdk/src/hotspot/share/ci/ciConstant.hpp
Line 134 in c5c8c06
| return as_object()->is_loaded(); |
Another issue is that ciConstant.hpp includes ciNullObject.hpp which inherits from ciObject. I worked around both issues by moving code and includes around.
I moved the code to ciObject, as you proposed, and extended it to include final fields as well. Let me know what you think.
Testing now revealed a new "Not monotonic" assert that seems to be triggered by caching non-stable final fields. I'll investigate but I'm not able to reproduce it yet.
Thanks @TobiHartmann for explanations. Caching looks correct. First intercept of constant field folding is dring parsing when a field access is made and later on gvn folds the constant field loads. Similar change could be done for C1 jdk/src/hotspot/share/c1/c1_GraphBuilder.cpp Line 1797 in af8d3fb
But given that problem is only seen during aggressive optimization like CCP I agree with @iwanowww's suggestion of restricting the scope of fix to C2. |
Not necessarily. I spotted the following for static final fields: As it is now, you can't avoid the state transition when doing ... and then put a call to check the cache first before trying to load a value from the field. There should be a |
Nice catch! |
|
Thanks again, Vladimir. I updated the change according to your suggestions. |
iwanowww
left a comment
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.
Overall, looks good!
|
Thanks again, Vladimir. I incorporated both your suggestions. |
iwanowww
left a comment
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.
Looks good.
|
Thanks Vladimir. @vnkozlov, @chhagedorn, @jatin-bhateja are you okay with the latest version as well? |
vnkozlov
left a comment
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 am fine with moving caching to CI.
As I see (may be wrong) you allow C2 to see all cached values which may introducing inconsistency in generated code. I am not sure how PhaseCPP::verify_type() works in such case.
I'm not sure if I understand your concern, what do you mean by "all cached values"? The cache will only contain values observed during the current compilation (it's lifetime is limited by the lifetime of |
After reading Vladimir's and your discussion and looking on code I see that you check value in cache before adding it to cache. So my concern is mute. But why do you need |
Because there's only a very limited number of constant fields per compilation, so I thought a simple array is good enough. Do you prefer a hash map ( |
My mistake. I thought you collected values per field. But you collected all constant fields per object. Good. GrowableArray is perfectly fine for that. Finally I got this ;^) One last question. Do I understand correctly that static fields constant value is already handled by |
|
Thanks again, Vladimir.
Yes, for example for C2, final static fields are handled via |
vnkozlov
left a comment
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.
Good. Thank you for answering all my questions.
|
Thanks, Vladimir! |
|
/integrate |
|
Going to push as commit cae577a.
Your commit was automatically rebased without conflicts. |
|
@TobiHartmann Pushed as commit cae577a. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
We hit a "not monotonic" assert because the new type of a load from a stable final field is more narrow than the old type which contradicts the assumption that types should only go from TOP to BOTTOM during CCP:
old:
narrowoop: java/lang/Integer:BotPTR:exact *new:
narrowoop: java/lang/Integer java.lang.Integer {0x000000062c41e548} ...or
old:
narrowoop: java/lang/Integer java.lang.Integer {0x000000062c41e538} ...new:
narrowoop: java/lang/Integer java.lang.Integer {0x000000062c41e548} ...The problem is that a stable field can be (re-)initialized during compilation and since the value is not cached, contradicting types can be observed. In
LoadNode::Value, we re-read the field value each time:jdk/src/hotspot/share/opto/memnode.cpp
Lines 1994 to 1997 in 8723847
jdk/src/hotspot/share/opto/type.cpp
Lines 332 to 337 in 8723847
The same problem exists for loads from stable arrays:
jdk/src/hotspot/share/opto/memnode.cpp
Line 1923 in 8723847
Caching the field value is not feasible as it would require a cache per ciInstance for all the fields and per ciArray for all the elements. Alternatively, we could keep track of the lookup and only do it once but that would also be lots of additional complexity for a benign issue.
Instead, I propose to skip verification during CCP when folding loads from stable fields. Non-stable, constant fields are not affected as
nullis a valid value for them and they would already be folded before CCP.Thanks,
Tobias
Progress
Issue
Reviewers
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jdk pull/11861/head:pull/11861$ git checkout pull/11861Update a local copy of the PR:
$ git checkout pull/11861$ git pull https://git.openjdk.org/jdk pull/11861/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 11861View PR using the GUI difftool:
$ git pr show -t 11861Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/11861.diff