Skip to content

Conversation

@cushon
Copy link
Contributor

@cushon cushon commented Mar 3, 2025

Hello, please consider this fix for JDK-8350563 contributed by my colleague Matthias Ernst.

#22856 introduced a new Value() optimization for the pattern AndIL(Con, Mask).
This optimization can look through CastNodes, and therefore requires additional logic in CCP to push these
transitive uses to the worklist.

The optimization is closely related to analogous optimizations for SHIFT nodes, and we also extend the existing logic for
CCP worklist handling: the current logic is "if the shift input to a SHIFT node changes, push indirect AND node uses to the CCP worklist".
We extend it by adding "if the (new) type of a node is an IntegerType that is_con, ... to the predicate.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8350563: C2 compilation fails because PhaseCCP does not reach a fixpoint (Bug - P3)

Reviewers

Contributors

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/23871/head:pull/23871
$ git checkout pull/23871

Update a local copy of the PR:
$ git checkout pull/23871
$ git pull https://git.openjdk.org/jdk.git pull/23871/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 23871

View PR using the GUI difftool:
$ git pr show -t 23871

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/23871.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Mar 3, 2025

👋 Welcome back cushon! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@cushon
Copy link
Contributor Author

cushon commented Mar 3, 2025

/contributor add Matthias Ernst [email protected]

@openjdk
Copy link

openjdk bot commented Mar 3, 2025

@cushon 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:

8350563: C2 compilation fails because PhaseCCP does not reach a fixpoint

Co-authored-by: Matthias Ernst <[email protected]>
Reviewed-by: chagedorn, epeter

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 73 new commits pushed to the master branch:

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 master branch, type /integrate in a new comment.

@openjdk openjdk bot added the rfr Pull request is ready for review label Mar 3, 2025
@openjdk
Copy link

openjdk bot commented Mar 3, 2025

@cushon
Contributor Matthias Ernst <[email protected]> successfully added.

@openjdk
Copy link

openjdk bot commented Mar 3, 2025

@cushon The following label will be automatically applied to this pull request:

  • hotspot-compiler

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.

@mlbridge
Copy link

mlbridge bot commented Mar 3, 2025

@eme64
Copy link
Contributor

eme64 commented Mar 4, 2025

@cushon @mernst-github Thanks for working on the fix!

Can you please add a description to the PR (and if possible also on JIRA) to explain what the issue is, and how you are fixing it?
Is there a regression test that reproduces this reliably?

@cushon
Copy link
Contributor Author

cushon commented Mar 5, 2025

Thanks for taking a look!

Can you please add a description to the PR (and if possible also on JIRA) to explain what the issue is, and how you are fixing it?

Done

Is there a regression test that reproduces this reliably?

Matthias reports:

Re: regtest: I don't currently have a standalone regtest, I am using java/lang/Character/CheckProp.java from the issue report to reproduce the failure and verify the fix. I can try and extract the failure condition into a standalone test.
Does the fix look reasonable?

Copy link
Contributor

@eme64 eme64 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates!

I have some comments about the fix. Generally extending push_and is the right approach, I think :)

Comment on lines 2002 to 2004
if (new_type != nullptr && new_type->is_con()) {
// Pattern: parent (now constant) -> (ConstraintCast | ConvI2L)* -> And
to_push = parent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?
Do we not already push all uses of parent further up?

  1881 void PhaseCCP::push_child_nodes_to_worklist(Unique_Node_List& worklist, Node* n) const {
  1882   for (DUIterator_Fast imax, i = n->fast_outs(imax); i < imax; i++) {
  1883     Node* use = n->fast_out(i);
  1884     push_if_not_bottom_type(worklist, use);
  1885     push_more_uses(worklist, n, use);                                                                                                                                                                                          
  1886   }
  1887 }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What am I missing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that would not traverse through ContraintCast and ConvI2L. I see.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, why not just add these two cases to the case below?
((use_op == Op_LShiftI || use_op == Op_LShiftL) && use->in(2) == parent
or
n->is_ConstraintCast() || n->Opcode() == Op_ConvI2L ?

That would allow you to traverse this pattern too:
// Pattern: parent (now constant) -> (ConstraintCast | ConvI2L)* -> And

I think this would be closer to existing patterns.

What do you think?

push_if_not_bottom_type(worklist, n);
}
};
to_push->visit_uses(push_and_uses_to_worklist, is_boundary);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why not just call this line in two places, rather than having to work with to_push? Would that not be less code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matthias reports:

That was purely a stylistic choice - the two cases can be integrated, we can push from "use" in both cases. Updated.

@cushon
Copy link
Contributor Author

cushon commented Mar 6, 2025

Updated to address review comments, and add a test

Copy link
Contributor

@eme64 eme64 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates! It looks much better :)

@@ -0,0 +1,51 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test should probably be moved to test/hotspot/jtreg/compiler/ccp/, that would be more specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// Pattern: parent -> LShift (use) -> (ConstraintCast | ConvI2L)* -> And
((use_op == Op_LShiftI || use_op == Op_LShiftL) && use->in(2) == parent)) {

auto push_and_uses_to_worklist = [&](Node* n) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, this looks much better. I suggest you rename new_type -> parent_type, just to keep things consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@chhagedorn chhagedorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small comments, otherwise, it looks good to me.

Comment on lines 28 to 31
* @run main/othervm -Xbatch -XX:-TieredCompilation compiler.c2.TestAndConZeroCCP
* @run driver compiler.c2.TestAndConZeroCCP
*/
package compiler.c2;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should update this to ccp now that you moved the test and use main instead of driver. Otherwise, @run driver is never executed with additionally passed in flags, for example in higher tier.

Suggested change
* @run main/othervm -Xbatch -XX:-TieredCompilation compiler.c2.TestAndConZeroCCP
* @run driver compiler.c2.TestAndConZeroCCP
*/
package compiler.c2;
* @run main/othervm -Xbatch -XX:-TieredCompilation compiler.ccp.TestAndConZeroCCP
* @run driver compiler.ccp.TestAndConZeroCCP
*/
package compiler.ccp;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


public class TestAndConZeroCCP {

public static void main(String[] args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use a 4 space indentation for Java tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Mar 19, 2025
@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Mar 19, 2025
@chhagedorn
Copy link
Member

There was a test failure in a bigger test that I cannot share. But I was able to extract a simple reproducer:

public class Test {

    public static void main(String[] args) {
        test();
    }

    static void test() {
        Integer.parseInt("1");
    }
}

Run with (might need to run multiple times or increase RepeatCompilation count since it is dependent on the seed):

java -XX:RepeatCompilation=300 -XX:+StressIGVN -XX:+StressCCP -Xcomp -XX:CompileOnly=*Integer::parseInt Test.java

Output:

  304  ConI  === 0  [[ 506 ]]  #int:255
  996  CastII  === 461 453  [[ 557 546 535 524 1034 506 ]]  #int:-256..127 extra types: {0:int:-256} strong dependency !orig=[478] !jvms: Integer::parseInt @ bci:144 (line 550)
  506  AndI  === _ 996 304  [[ 507 ]]  !jvms: Integer::parseInt @ bci:170 (line 552)
told = int:0
tnew = top
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (/opt/mach5/mesos/work_dir/slaves/2a0767be-5c1b-4719-9b4f-f71b11137965-S769/frameworks/1735e8a2-a1db-478c-8104-60c8b0af87dd-0196/executors/1111ed35-4920-43a2-b273-caace0afc18d/runs/09365b8c-550a-453d-9bbb-ff3c7dc04263/workspace/open/src/hotspot/share/opto/phaseX.cpp:1790), pid=196209, tid=196227
#  fatal error: Not monotonic
#
# JRE version: Java(TM) SE Runtime Environment (25.0) (fastdebug build 25-internal-LTS-2025-03-19-2013139.christian.hagedorn.jdk-test)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (fastdebug 25-internal-LTS-2025-03-19-2013139.christian.hagedorn.jdk-test, compiled mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
# Problematic frame:
# V  [libjvm.so+0x16a26c9]  PhaseCCP::verify_type(Node*, Type const*, Type const*)+0x169

@cushon
Copy link
Contributor Author

cushon commented Mar 20, 2025

From Matthias

Thank you Christian for that reproducer! Unless you've already tried, I think I'll first try and verify whether it's an already present issue at HEAD and this fix is simply incomplete (related to the CCP vs IGVN discussion on #22856 (comment)) or whether it's being caused by this fix PR.
I suspect it might be the former, in which case the question would be whether we want to go ahead with this fix for the CCP worklist to reduce noise?

@chhagedorn
Copy link
Member

I've tried it with latest master but could not reproduce it. Adding nodes to the CCP worklist should not directly be an issue. So, I expect this patch just revealed an existing issue with JDK-8346664.

I suspect it might be the former, in which case the question would be whether we want to go ahead with this fix for the CCP worklist to reduce noise?

Yes, I guess we can continue with this patch to reduce the noise in the CI and follow up with another bug fix to address the problem I reported. Can you file a bug accordingly?

 322  Phi  === 303 119 255  [[ 399 388 351 751 366 377 ]]  #int:-256..127 !jvms: Integer::parseInt @ bci:151 (line 625)

 While this Phi dumps as "#int:-256..127", `phase->type(expr)` returns a type that is_con -256.
@cushon
Copy link
Contributor Author

cushon commented Mar 31, 2025

From Matthias


I was able to reproduce the issue as is by Christian and I have a fix - with some caveats since I only have a partial understanding of what's happening.

Here's what I know:

In order to simplify EXPR & MASK, AndINode::Value() compares the trailing zero bits of EXPR against the width of MASK.
For this, we use phase->type(expr)->is_con().

What I observe for the Integer.parseInt reproducer is that expr dumps as a phi node with type #int:-256...127, but phase->type(expr) returns a type that is_con() with value -256.
In consequence, the AND(phi-node, mask) gets optimized to zero.

Concretely, my understanding is that the node is "digit" here:
https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Integer.java#L618,L622,L627
618 int digit = ~0xFF; lo = -256
622 ..if.. { digit = digit(firstChar, radix); .. } assumes Latin1 => byte => hi = 127 from here: https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/CharacterDataLatin1.java.template#L233
627: int result = -(digit & 0xFF);

I can only guess why we'd get an is_con() type for this node, I assume it's a speculative optimization, but I would expect that to happen all the time.
Why reducing this phi node to zero would cause an issue, though, I'm out of my depth there.

Anyway, my first instinct is to replace the constant check in the optimization phase->type(expr)->is_con() with an explicit opcode check (== OP_ConI || OP_ConL).
This a) fixes the crash, b) passes all tests we added/changed the new optimization, so it doesn't undo anything we were trying to accomplish in the first place.
I've pushed a corresponding commit, ptal:

-  if (type->is_con()) {
+  if (expr->Opcode() == Op_ConI || expr->Opcode() == Op_ConL) {

It does feel like it's addressing a symptom though, not a cause.
That being said, the "And[IL]Node::Value" optimization for "const & mask => 0" has always been a "happy byproduct", the actual goal was always the optimization in "::Ideal": "((expr + const) << shift) & mask => expr & mask",
which still works. LMK what you think.

Unfortunately, I have been unable to reproduce this outside of jl.Integer.parseInt. Even when copying jl.Integer.parseInt to my own method, I wasn't able to trigger the crash.

Matthias

@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Mar 31, 2025
@chhagedorn
Copy link
Member

Thanks Matthias for having a look at the issue and proposing a fix! While this fix seems to work, I think we should address it slightly differently with an explicit bailout, though. Let's step back a bit:

CCP first sets all types to top and then tries to widen them (i.e. an optimistic approach) while IGVN does the opposite: We start by setting all types to bottom and then try to narrow them (i.e. a pessimistic approach).

The assert we've faced in CCP complains that we tried to narrow some type again which is against the rules of CCP - we can only widen types.

Now when CCP runs, we start with every type of every node at top. When visiting AndI at some point, we see what you reported above:

What I observe for the Integer.parseInt reproducer is that expr dumps as a phi node with type #int:-256...127, but phase->type(expr) returns a type that is_con() with value -256.

That is perfectly fine. What happened here is that only one input of the phi with type #int:-256 is non-top. The other inputs are still top (i.e. not processed in CCP, yet). Therefore, the phi's type is set to #int:-256. Note that the TypeNode::_type field of the phi is still set to the type we had before CCP, i.e. #int:-256...127 . In CCP, we use PhaseValues::_types which are set to top in the beginning and we leave TypeNode::_type unchanged during the analysis.

As a consequence this can happen when having a phi and only looking at the currently tracked CCP types:

In consequence, the AND(phi-node, mask) gets optimized to zero.

Let's look at the output of the failure:

  304  ConI  === 0  [[ 506 ]]  #int:255
  996  CastII  === 461 453  [[ 557 546 535 524 1034 506 ]]  #int:-256..127 extra types: {0:int:-256} strong dependency !orig=[478] !jvms: Integer::parseInt @ bci:144 (line 550)
  506  AndI  === _ 996 304  [[ 507 ]]  !jvms: Integer::parseInt @ bci:170 (line 552)
told = int:0
tnew = top

it looks like we first optimized AndI to zero (i.e. told) and then set it to top again in a later Value() call in CCP (i.e. tnew). This is a violation of the rules for CCP. When we suddenly see top again, it suggests that we prematurely applied an optimization while one of the involved inputs was actually still top. This looks wrong and we should have waited until all the involved inputs are non-top.

When looking at the code, we check that mask is an integer type and thus non-top here:

static bool AndIL_is_zero_element_under_mask(const PhaseGVN* phase, const Node* expr, const Node* mask, BasicType bt) {
// When the mask is negative, it has the most significant bit set.
const TypeInteger* mask_t = phase->type(mask)->isa_integer(bt);
if (mask_t == nullptr || mask_t->lo_as_long() < 0) {
return false;
}

But it looks like we miss that for expr when it is a cast node (which is 996 CastII in the failing test). We pass expr to AndIL_min_trailing_zeros() and then uncast it and only then check if it is a proper integer type:

static jint AndIL_min_trailing_zeros(const PhaseGVN* phase, const Node* expr, BasicType bt) {
expr = expr->uncast();
const TypeInteger* type = phase->type(expr)->isa_integer(bt);
if (type == nullptr) {
return 0;
}

So, if the type of 996 CastII in CCP is still top, we skip it with uncast() and then check the phi above which has first the constant type #int:-256. We can apply the optimization to return type zero. When later updating the type of the phi to #int:-256...127, we can no longer apply the optimization and fall back to MulNode::Value() where we return top because the input 996 CastII is still top:

const Type* MulNode::Value(PhaseGVN* phase) const {
const Type *t1 = phase->type( in(1) );
const Type *t2 = phase->type( in(2) );

We find top which is narrower than type zero and we fail with the assert.

Long story short, you should check for expr being top before uncasting it. This was hard to see and is only a problem in CCP.

I suggest to add the small reproducer as additional test case.

cushon and others added 3 commits April 5, 2025 11:31
A child phi node may transition from con to non-con, making the AND node transition back from "0" to its current type. If that current type is still TOP we're in violation of monotonicity. Therefore, don't apply optimization if AND is not integer yet.
@cushon
Copy link
Contributor Author

cushon commented Apr 5, 2025

From Matthias


Thank you Christian for the excellent explanation. I understand how the phi going from con to non-con makes the AND node go from 0 back to top. I've pushed a commit with the fix and the reproducer; verified the latter only passes w/ the fix.

Copy link
Member

@chhagedorn chhagedorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, you're welcome! The fix looks good to me now. I will emit some internal testing again when you merged the tests together. Then I think it's good to go :-)


public class TestAndConZeroMonotonic {

public static void main(String[] args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's quite an easy test, I suggest to merge the two test files together by calling Integer::parseInt() directly instead. You can just add another @run statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Apr 7, 2025
@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Apr 7, 2025
Copy link
Member

@chhagedorn chhagedorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the merge! I'll submit some testing and report back once it's finished.

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Apr 7, 2025
@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Apr 8, 2025
Copy link
Member

@chhagedorn chhagedorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing looked good, thanks for the updates!

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Apr 9, 2025
@cushon
Copy link
Contributor Author

cushon commented Apr 9, 2025

From Matthias: Thank you all! Will integrate this now. I hope this addresses all remaining issues with the optimization.

@cushon
Copy link
Contributor Author

cushon commented Apr 9, 2025

/integrate

@openjdk
Copy link

openjdk bot commented Apr 9, 2025

Going to push as commit 4954a33.
Since your change was applied there have been 96 commits pushed to the master branch:

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label Apr 9, 2025
@openjdk openjdk bot closed this Apr 9, 2025
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review labels Apr 9, 2025
@openjdk
Copy link

openjdk bot commented Apr 9, 2025

@cushon Pushed as commit 4954a33.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hotspot-compiler [email protected] integrated Pull request has been integrated

Development

Successfully merging this pull request may close these issues.

4 participants