Skip to content

Conversation

@SirYwell
Copy link
Member

@SirYwell SirYwell commented May 15, 2025

This change improves the precision of the Mod(I|L)Node::Value() functions.

I reordered the structure a bit. First, we handle constants, afterwards, we handle ranges. The bottom checks seem to be excessive (Type::BOTTOM is covered by using isa_(int|long)(), the local bottom is just the full range). Given we can even give reasonable bounds if only one input has any bounds, we don't want to return early.
The changes after that are commented. Please let me know if the explanations are good, or if you have any suggestions.

Monotonicity

Before, a 0 divisor resulted in Type(Int|Long)::POS. Initially I wanted to keep it this way, but that violates monotonicity during PhaseCCP. As an example, if we see a 0 divisor first and a 3 afterwards, we might try to go from >=0 to -2..2, but the meet of these would be >=-2 rather than -2..2. Using Type(Int|Long)::ZERO instead (zero is always in the resulting value if we cover a range).

Testing

I added tests for cases around the relevant bounds. I also ran tier1, tier2, and tier3 but didn't see any related failures after addressing the monotonicity problem described above (I'm having a few unrelated failures on my system currently, so separate testing would be appreciated in case I missed something).

Please review and let me know what you think.

Other

The UMod(I|L)Nodes were adjusted to be more in line with its signed variants. This change diverges them again, but similar improvements could be made after #17508.

During experimenting with these changes, I stumbled upon a few things that aren't directly related to this change, but might be worth to further look into:

  • If the divisor is a constant, we will directly replace the Mod(I|L)Node with more but less expensive nodes in ::Ideal(). Type analysis for these nodes combined is less precise, means we miss potential cases were this would help e.g., removing range checks. Would it make sense to delay the replacement?
  • To force non-negative ranges, I'm using char. I noticed that method parameters of sub-int integer types all fall back to TypeInt::INT. This seems to be an intentional change of 200784d. The bug report is private, so I can't really judge if that part is necessary, but it seems odd.

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-8356813: Improve Mod(I|L)Node::Value (Enhancement - P4)

Reviewers

Reviewing

Using git

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

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

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 25254

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

Using diff file

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

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented May 15, 2025

👋 Welcome back hgreule! 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.

@openjdk
Copy link

openjdk bot commented May 15, 2025

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

8356813: Improve Mod(I|L)Node::Value

Reviewed-by: epeter, qamai

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 497 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 changed the title 8356813 8356813: Improve Mod(I|L)Node::Value May 15, 2025
@openjdk
Copy link

openjdk bot commented May 15, 2025

@SirYwell 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.

@SirYwell SirYwell marked this pull request as ready for review May 15, 2025 16:51
@openjdk openjdk bot added the rfr Pull request is ready for review label May 15, 2025
@mlbridge
Copy link

mlbridge bot commented May 15, 2025

@merykitty
Copy link
Member

Using Type(Int|Long)::ZERO instead (zero is always in the resulting value if we cover a range).

Can we return Type::TOP instead?

Besides, #17508 should be merged right after JDK-25 folk, do you want to wait for it first?

@SirYwell
Copy link
Member Author

Using Type(Int|Long)::ZERO instead (zero is always in the resulting value if we cover a range).

Can we return Type::TOP instead?

That should work too and might be more intuitive. I assume there also isn't much benefit in constant-folding users of the mod if the mod is known to fail (which seems to be the only benefit of not returning TOP?).

Besides, #17508 should be merged right after JDK-25 folk, do you want to wait for it first?

We can wait if it makes sense to do the unsigned variants here too, but I'm also fine with doing it separately.

Copy link
Contributor

@mhaessig mhaessig left a comment

Choose a reason for hiding this comment

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

Thank you for working on this, @SirYwell. I especially like the citations directly from the spec to motivate and justify the optimizations.

I commented only on the int side of things, but the comments apply equally to the long changes.

You exclude zero from the dividend magnitude only based on the constant check. That is not correct. You have to check the range as well to exclude zero. Hence, it would also be nice to have test cases where the value is known to be in a given range in the ideal graph. To get such a value, you can call array.length(), which is always >=0, or use Parse::sharpen_type_after_if():

switch (btest) {
case BoolTest::eq: // Constant test?
{
const Type* tboth = tcon->join_speculative(tval);
if (tboth == tval) break; // Nothing to gain.
if (tcon->isa_int()) {
ccast = new CastIINode(control(), val, tboth);
} else if (tcon == TypePtr::NULL_PTR) {
// Cast to null, but keep the pointer identity temporarily live.
ccast = new CastPPNode(control(), val, tboth);
} else {
const TypeF* tf = tcon->isa_float_constant();
const TypeD* td = tcon->isa_double_constant();
// Exclude tests vs float/double 0 as these could be
// either +0 or -0. Just because you are equal to +0
// doesn't mean you ARE +0!
// Note, following code also replaces Long and Oop values.
if ((!tf || tf->_f != 0.0) &&
(!td || td->_d != 0.0))
cast = con; // Replace non-constant val by con.
}
}
break;

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.

@SirYwell Thanks for looking into this, that looks promising!

I have two bigger comments:

  • Could we unify the L and I code, either using C++ templating or BasicType? It would reduce code duplication.
  • Can we have some tests where the input ranges are random as well, and where we check the output ranges with some comparisons?

Copied from the code comment:

Nice work with the examples you already have, and randomizing some of it!

I would like to see one more generalized test.

  • compute res = lhs % rhs
  • Truncate both lhs and rhs with randomly produced bounds from Generators, like this: lhs = Math.max(lo, Math.min(hi, lhs)).
  • Below, add all sorts of comparisons with random constants, like this: if (res < CON) { sum += 1; }. If the output range is wrong, this could wrongly constant fold, and allow us to catch that.

Then fuzz the generated method a few times with random inputs for lhs and rhs, and check that the sum and res value are the same for compiled and interpreted code.

I hope that makes sense :)
This is currently my best method to check if ranges are correct, and I think it is quite important because often tests are only written with constants in mind, but less so with ranges, and then we mess up the ranges because it is just too tricky.

This is an example, where I asked someone to try this out as well:
https://github.com/openjdk/jdk/pull/23089/files#diff-12bebea175a260a6ab62c22a3681ccae0c3d9027900d2fdbd8c5e856ae7d1123R404-R422

@SirYwell
Copy link
Member Author

Thanks @eme64. I unified the code now using BasicType. This works well because we can use the jlong operations everywhere (if I didn't miss something, please verify that claim). You can probably compare it to the unsigned_mod_value that is currently templated. I assume using BasicType there would be more involved because signed -> unsigned conversion depends on the actual type (i.e. the unsigned value of -1 is different for long vs int).

I'll also look into your suggestions for the tests, thanks for the input there.

@SirYwell
Copy link
Member Author

@eme64 I merged master and hopefully addressed your latest comments. Now that we have #17508 integrated, I could also directly update the unsigned variant, but I'm also fine with doing that separately. WDYT?

I also checked the constant folding part again (or generally whenever the RHS is a constant), these code paths are indeed not used by PhaseGVN directly (but by PhaseCCP and PhaseIdealLoop). That makes it a bit difficult to test that part properly.

Comment on lines 1234 to 1236
// We must be modulo'ing 2 int constants.
// Check for min_jlong % '-1', result is defined to be '0'
// We don't need to check for min_jint % '-1' as its result is defined when using jlong.
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems both cases are "defined"... so it sounds a little strange when you say ... as its result is defined when using jlong. Both are "defined", it would be nice if you said explicitly "how" they are defined.

But wait... how does this work. We used to do the same trick above for min_jint when using Jint, correct?

  // We must be modulo'ing 2 float constants.
  // Check for min_jint % '-1', result is defined to be '0'.
  if( i1->get_con() == min_jint && i2->get_con() == -1 )
    return TypeInt::ZERO;

Is this case here really handling that? It doesn't look like it.
Do we have tests for all these cases?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, seems we have discussed this before... Maybe it is best to just keep the old behavior and do the test for min_jint as well if we have T_INT. I'd rather be safe.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can add min_jint as a special case again. But I just had a different idea, as x % -1 == 0 for any x, I could also generalize the check and only test for -1. WDYT?

Comment on lines 1242 to 1244
// The magnitude of the divisor is in range [1, 2^31] or [1, 2^63], depending on the BasicType.
// We know it isn't 0 as we handled that above.
// That means at least one value is nonzero, so its absolute value is bigger than zero.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually struggling to follow this here. Can you define "magnitude" for the reader? Maybe there is some JVMS definition you can mention. And which "value" are you refering to, that is nonzero here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// The magnitude of the divisor is in range [1, 2^31] or [1, 2^63], depending on the BasicType.
// We know it isn't 0 as we handled that above.
// That means at least one value is nonzero, so its absolute value is bigger than zero.
// We checked that t2 is not the zero constant. Hence at least i2->_lo or i2->_hi must be non-zero,
// and hence its its absoute value is bigger than zero. Hence, the magnitude of the divisor (i.e. the
// largest absolute value for any value in i2) must be in the range [1, 2^31] or [1, 2^63], depending
// on the BasicType.

Copy link
Member Author

Choose a reason for hiding this comment

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

Magnitude is what the JVMS uses, that's why I used it. But I like your suggested wording, I'll adapt it.

@eme64
Copy link
Contributor

eme64 commented Jun 16, 2025

@eme64 I merged master and hopefully addressed your latest comments. Now that we have #17508 integrated, I could also directly update the unsigned variant, but I'm also fine with doing that separately. WDYT?

I also checked the constant folding part again (or generally whenever the RHS is a constant), these code paths are indeed not used by PhaseGVN directly (but by PhaseCCP and PhaseIdealLoop). That makes it a bit difficult to test that part properly.

Let's keep the patch as it is. With #17508 we will have to also probably refactor and add more tests, if we want to do any unsigned and known-bit optimizations.


@SirYwell Thanks for the updates, I had a few more comments, but we are getting there :)

@SirYwell
Copy link
Member Author

Looks really good now. I think we can almost integrate now.

Thanks for the review :)

One thing I'm wondering: could this be extended to UModI/L? That can of course be a separate RFE as well. And yet another idea: could we use the known bits? See #17508.

Yes, UModI/L could be done now in a similar fashion using usigned ranges. I can open an RFE later. I'm not sure if we can get more precise bitwise information than what the canonicalization already does. I don't see anything obvious there at least.

Can you show some examples? Filing an RFE would surely not be wrong.

https://gist.github.com/SirYwell/151a48c90d12593bf500028389bdd07c this is an example. (Currently, we don't detect patterns like Math.floorMod(...), so I'm just casting to char to get a nonnegative value).
In the patched version, I added a bailout in transform_int_divide to to delay the transformation to IGVN. This way, we actually run ModI::Value() and get a type that lets us eliminate the CmpU. There are probably better ways to achieve that :) I wonder if there are more such scenarios, and if it's worth to calculate some initial type before Ideal()...

@SirYwell
Copy link
Member Author

SirYwell commented Sep 2, 2025

@eme64 gentle ping in case you missed my latest changes :)
Please let me know if there is more to do.

@SirYwell
Copy link
Member Author

SirYwell commented Sep 3, 2025

I also filed https://bugs.openjdk.org/browse/JDK-8366815 now regarding the early transformation of div/mod by constants.

@eme64
Copy link
Contributor

eme64 commented Sep 9, 2025

Thanks for filing the issue! I left some comments there. We could delay div/mod by constants to after loop opts. And we could even optimize div/mod in loops that have loop-invariant divisor ;)

@eme64
Copy link
Contributor

eme64 commented Sep 9, 2025

@SirYwell The changes look good to me, thanks for working on this!

I'll now run some internal testing, before approving. Please ping me again in 24h if I don't report back by then :)

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.

Tests pass, approved 😊

@merykitty @mhaessig your turn 😉

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Sep 10, 2025
Copy link
Member

@merykitty merykitty left a comment

Choose a reason for hiding this comment

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

Nice consolidation also. I have only some small style suggestion.

// Mod by zero? Throw exception at runtime!
if( !i2->get_con() ) return TypeInt::POS;
if (t2 == TypeInteger::zero(bt)) {
return TypeInt::TOP;
Copy link
Member

Choose a reason for hiding this comment

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

TypeInt::TOP is actually Type::TOP

lo = MAX2(lo, i1->lo_as_long());
hi = MIN2(hi, i1->hi_as_long());
}
return TypeInteger::make(lo, hi, MAX2(i1->_widen,i2->_widen), bt);
Copy link
Member

Choose a reason for hiding this comment

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

Small style: space after comma.

return TypeInt::ZERO;
const TypeInteger* i1 = t1->isa_integer(bt);
const TypeInteger* i2 = t2->isa_integer(bt);
if (i1 == nullptr || i2 == nullptr) {
Copy link
Member

Choose a reason for hiding this comment

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

If they are not TOP here, isa_integer should never return nullptr, it's better to do an assert here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess using is_integer directly might make sense then?

@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Sep 11, 2025
@SirYwell
Copy link
Member Author

@merykitty thanks, I hopefully addressed your comments :)

@eme64 do you want to re-run the tests once again?

@eme64
Copy link
Contributor

eme64 commented Sep 12, 2025

@SirYwell Launching tests 🚀

@SirYwell
Copy link
Member Author

I noticed one parameter was unused, I removed it now. This shouldn't affect testing I guess.

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.

Testing looks good. Minor changes should be ok, as long as GitHub Actions passes.

Thanks for all the work @SirYwell !

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Sep 16, 2025
@SirYwell
Copy link
Member Author

Thanks @eme64! Do I need another re-approval from @merykitty or are we ready to integrate?

@eme64
Copy link
Contributor

eme64 commented Sep 16, 2025

@SirYwell @merykitty Let's give him 24h. If he does not respond, you can integrate in my opinion.

@SirYwell
Copy link
Member Author

Thanks everyone for the patience and the reviews :)

/integrate

@openjdk
Copy link

openjdk bot commented Sep 16, 2025

Going to push as commit c7f014e.
Since your change was applied there have been 497 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 Sep 16, 2025
@openjdk openjdk bot closed this Sep 16, 2025
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review labels Sep 16, 2025
@openjdk
Copy link

openjdk bot commented Sep 16, 2025

@SirYwell Pushed as commit c7f014e.

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

@SirYwell SirYwell deleted the improve-mod-value branch September 16, 2025 12:37
// We always generate the dynamic check for 0.
// 0 MOD X is 0
if( t1 == TypeInt::ZERO ) return TypeInt::ZERO;
if (t1 == TypeInteger::zero(bt)) { return t1; }
Copy link
Member

Choose a reason for hiding this comment

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

I think the culprit for JDK-8356813 is this place. We need to check for the divisor being a constant 0 and return Type::TOP before this check and the check below.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I already worked a bit on it, see https://github.com/SirYwell/jdk/tree/fix/mod-not-monotonic but I didn't have time to create a PR yet.

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