Skip to content

8371716: C2: Phi node fails Value()'s verification when speculative types clash#28331

Closed
marc-chevalier wants to merge 9 commits intoopenjdk:masterfrom
marc-chevalier:JDK-8371716
Closed

8371716: C2: Phi node fails Value()'s verification when speculative types clash#28331
marc-chevalier wants to merge 9 commits intoopenjdk:masterfrom
marc-chevalier:JDK-8371716

Conversation

@marc-chevalier
Copy link
Member

@marc-chevalier marc-chevalier commented Nov 14, 2025

This bug was originally found and reported as a Valhalla problem. It quickly became apparent it has no reason to be Valhalla-specific, while I couldn't prove so. Roland managed to make a mainline reproducer. The explanation details my Valhalla investigation, but it has nothing to do with value classes anyway.

The proposed solution seems somewhat controversial. See openjdk/valhalla#1717 for some previous discussion. Before polishing the PR, I'd like to reach an agreement on the way to go.

Analysis

Obervationally

IGVN

During IGVN, in PhiNode::Value, a PhiNode has 2 inputs. Their types are:

in(1): java/lang/Object * (speculative=TestSpeculativeTypes$C2:NotNull:exact * (inline_depth=3))
in(2): null

We compute the join (HS' meet):

const Type *t = Type::TOP; // Merged type starting value
for (uint i = 1; i < req(); ++i) {// For all paths in
// Reachable control path?
if (r->in(i) && phase->type(r->in(i)) == Type::CONTROL) {
const Type* ti = phase->type(in(i));
t = t->meet_speculative(ti);
}
}

t=java/lang/Object * (speculative=compiler/igvn/ClashingSpeculativeTypePhiNode$C2:exact *)

But the current _type (of the PhiNode as a TypeNode) is

_type=java/lang/Object * (speculative=compiler/igvn/ClashingSpeculativeTypePhiNode$C1:exact *)

We filter t by _type

const Type* ft = t->filter_speculative(_type); // Worst case type

and we get

ft=java/lang/Object *

which is what we return. After the end of Value, the returned becomes the new PhiNode's _type.

const Type* t = k->Value(this);
assert(t != nullptr, "value sanity");
// Since I just called 'Value' to compute the set of run-time values
// for this Node, and 'Value' is non-local (and therefore expensive) I'll
// cache Value. Later requests for the local phase->type of this Node can
// use the cached Value instead of suffering with 'bottom_type'.
if (type_or_null(k) != t) {
#ifndef PRODUCT
inc_new_values();
set_progress();
#endif
set_type(k, t);
// If k is a TypeNode, capture any more-precise type permanently into Node
k->raise_bottom_type(t);

and
void Node::raise_bottom_type(const Type* new_type) {
if (is_Type()) {
TypeNode *n = this->as_Type();
if (VerifyAliases) {
assert(new_type->higher_equal_speculative(n->type()), "new type must refine old type");
}
n->set_type(new_type);

Verification

On verification, in(1), in(2) have the same value, so does t. But this time

_type=java/lang/Object *

and so after filtering t by (new) _type and we get

ft=java/lang/Object * (speculative=compiler/igvn/ClashingSpeculativeTypePhiNode$C2:exact *)

which is retuned. Verification gets angry because the new ft is not the same as the previous one.

But why?!

Details on type computation

In short, we are doing

t = typeof(in(1)) \/ typeof(in(2))
ft  = t /\ _type (* IGVN *)
ft' = t /\ ft    (* Verification *)

and observing that ft != ft'. It seems our lattice doesn't ensure (a /\ b) /\ b = a /\ b which is problematic for this kind of verfication that will just "try again and see if something change".

To me, the surprising fact was that the intersection

java/lang/Object * (speculative=compiler/igvn/ClashingSpeculativeTypePhiNode$C2:exact *)
/\
_type=java/lang/Object * (speculative=compiler/igvn/ClashingSpeculativeTypePhiNode$C1:exact *)
~>
java/lang/Object *

What happened to the speculative type? Both C1 and C2 are inheriting Object. So the code correctly find that the intersection of these speculative type is

ava/lang/Object:AnyNull * (flat in array),iid=top

The interesting part is that it's AnyNull: indeed, what else is a C1 and C2 at the same time? And then, above_centerline decides it's not useful enough (too precise, too close from HS' top/normal bottom) and remove the speculative type.

if (above_centerline(speculative()->ptr())) {
return no_spec;
}

But on the verification run, java/lang/Object * (speculative=compiler/igvn/ClashingSpeculativeTypePhiNode$C2:exact *) is intersected with the speculative type of java/lang/Object *, which is unknown (HS' bottom/normal top), so we are simply getting C2. If we did not discard AnyNull using above_centerline, we would have the intersection of C2 and AnyNull, giving AnyNull, which is indeed stable.

Reproducing

In Valhalla

This crash is quite rare because:

  1. it needs a specific speculative type setup, which depends heavily on timing
  2. if PhiNode::Value is called a second time, it will stabilize the _type field before verification.

To limitate the influence of 2., I've tested with an additional assert that would immediately do

const Type* ft_ = t->filter_speculative(ft);

in PhiNode::Value and compare ft and ft_. Indeed, we are never sure a run of Value is not the last one: it should always be legal to stop anywhere (even if in a particular case), it was going to run further.

With this extra check, the crash a bit more common, but still pretty rare. Tests that have been witness to crash then at least once:

  • compiler/valhalla/inlinetypes/TestCallingConvention.java
  • compiler/valhalla/inlinetypes/TestIntrinsics.java
  • compiler/valhalla/inlinetypes/TestArrays.java
  • compiler/valhalla/inlinetypes/TestBasicFunctionality.java

All in compiler/valhalla/inlinetypes while I was also testing with mainline tests. Suspicious, uh.

In mainline

With the aforementioned extra check, I've tried to see if it could happen on mainline since the involved code seems not to be valhalla-specific. Yet, nothing failed.

Fortunately, Roland crafted an example that reproduces in mainline (and Valhalla)! You'll find it as a test here.

Fixing?

I think changing the type system would be quite risky: it is all over the place. Also, fixing would require not to drop the speculative type when above_centerline. This might not be desirable. On top of the complexity and the associated risk, a too specific speculative type is rather useless. If we keep the too specific type around, we probably should ignore it where we make use of it. That's distributing the effort and open the door to inconsistencies. If we should ignore it, we might just as well drop it immediately. It is also dubious whether ordering requirement are meaningful for speculative type: they are not sound or complete. Moreover, one could argue that in the abstract, we don't even need a lattice or anything like that. A single poset whose functions are sound approximation of the concrete is enough. It is not uncommon that in the abstract world that a /\ b is not smaller than a.

For instance, in the co-interval domain (the whole universe E minus an interval), if we take a = E \ [1, 2] and b = E \ [3, 4], the concrete intersection would be E \ [1, 2] \ [3, 4] which is not allowed in our domain since it has 2 holes. If we want a sound approximation of that, we must remove at least one hole. We can then take E \ [1, 2] = a, E \ [3, 4] = b or even E (and of course, a lot of other things, with smaller holes than a or b). Whichever our abstract domain chooses, there is never both a /\ b < a and a /\ b < b. Indeed, this poset (like many real-world domains) is not a lattice, which doesn't keep us from speaking about soundness. An interesting and related fact is that there is no best abstraction of E \ [1, 2] \ [3, 4].

Maybe this digression about soundness rather hints that we should not compare speculative types during verification.

Finally, as a simple solution, one could simply run filter_speculative twice, that should be enough as the second filter will simply select the non empty speculative type if there is only one, and this one won't be above_centerline, or it would not exist as a speculative type already. To try to be a bit less aggressive, we can rather do that in case where we know it cannot be useful. If ft obtained from filter_speculative has no speculative type, but t has, we can know that it might be because it has been dropped, and computing t->filter_speculative(ft) could pick the speculative type of t. The speculative type can still be removed if the non-speculative type of ft is exact and non null for instance, but we've still reached a fixpoint, so it's correct, but a little bit too much work. This solution means that we are basically throwing away the speculative type in _type in case of clash. That sounds not shocking to me: if we get (hopefully) more accurate information on the inputs, it seems reasonable, or at least not shocking, to give up the previous speculative type if it clashes since it's not known to be correct. One can also phrase it as "we keep the speculative type of _type only if we don't have anything going against it".

This PR includes the last solution.

Thanks,
Marc


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-8371716: C2: Phi node fails Value()'s verification when speculative types clash (Bug - P4)

Reviewers

Contributors

  • Roland Westrelin <roland@openjdk.org>

Reviewing

Using git

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

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

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 28331

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

Using diff file

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

Using Webrev

Link to Webrev Comment

@marc-chevalier
Copy link
Member Author

/contributor add rwestrel

For the reproducer, now test.

@bridgekeeper
Copy link

bridgekeeper bot commented Nov 14, 2025

👋 Welcome back mchevalier! 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 Nov 14, 2025

@marc-chevalier 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:

8371716: C2: Phi node fails Value()'s verification when speculative types clash

Co-authored-by: Roland Westrelin <roland@openjdk.org>
Reviewed-by: roland, 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 4 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
Copy link

openjdk bot commented Nov 14, 2025

@marc-chevalier rwestrel was not found in the census.

Syntax: /contributor (add|remove) [@user | openjdk-user | Full Name <email@address>]. For example:

  • /contributor add @openjdk-bot
  • /contributor add duke
  • /contributor add J. Duke <duke@openjdk.org>

User names can only be used for users in the census associated with this repository. For other contributors you need to supply the full name and email address.

@openjdk openjdk bot added the hotspot-compiler hotspot-compiler-dev@openjdk.org label Nov 14, 2025
@openjdk
Copy link

openjdk bot commented Nov 14, 2025

@marc-chevalier 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.

@marc-chevalier
Copy link
Member Author

/contributor add @rwestrel

@openjdk
Copy link

openjdk bot commented Nov 14, 2025

@marc-chevalier
Contributor Roland Westrelin <roland@openjdk.org> successfully added.

@marc-chevalier marc-chevalier marked this pull request as ready for review November 14, 2025 20:01
@openjdk openjdk bot added the rfr Pull request is ready for review label Nov 14, 2025
@mlbridge
Copy link

mlbridge bot commented Nov 14, 2025

Webrevs

@eme64
Copy link
Contributor

eme64 commented Nov 17, 2025

@marc-chevalier I tried to review the issue at hand, but it is a little tricky because the regression test uses one set of class names and your description another (valhalla reproducer). Would it be possible to adjust the explanation to your mainline reproducer? Or maybe even just annotating your mainline reproducer with comments would help already.

I think it would be easier to review the PR once it is easy to follow the example.

Also: the issue name should be adjusted to say something a bit more specific about the issue. We have a lot of "missed optimization opportunity" issues, and it would be nice to be able to tell them apart easily ;)

if (ft->speculative() == nullptr && t->speculative() != nullptr) {
const Type* first_ft = ft;
ft = t->filter_speculative(first_ft);
#ifdef ASSERT
Copy link
Contributor

Choose a reason for hiding this comment

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

More of a flyby rather than a review but I was wondering if it would make sense to extract this assert block since it is the same as the one above.

Copy link
Member Author

Choose a reason for hiding this comment

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

We surely can. But I'd rather avoid premature clean up as it is very uncertain to me how this issue will evolve, and maybe fundamentally change.

@marc-chevalier
Copy link
Member Author

I've edited the description. I'd nevertheless suggest not to care too much about the test not to get sidetracked into details that are irrelevant to the issue. I actually regret I didn't write that symbolically.

@marc-chevalier marc-chevalier changed the title 8371716: C2 compilation fails with "Missed optimization opportunity in PhaseIterGVN" 8371716: C2: Phi node fails Value()'s verification when speculative types clash Nov 17, 2025
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 update. I now had a closer look, and got a little confused in multiple places. I'm not sure I have the right mental model yet.

I was also a little surprised by the fact that we do have inconsistent profiling. But maybe that should not be surprising? That's where a closer look at the reproducer would help me follow, and annotations in the test itself would be fantastic ;)

I have not yet looked at all the debug code in-depth, as I'd like to first understand the rest.


// In rare cases, `_type` and `t` have incompatible opinion on speculative type, resulting into a too small intersection
// (such as AnyNull), which is removed in cleanup_speculative. From that `ft` has empty speculative type. After the end
// of the current `Value` call, `ft` (that is returned) is becoming `_type`. If verification happens then, `t` would be the
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
// of the current `Value` call, `ft` (that is returned) is becoming `_type`. If verification happens then, `t` would be the
// of the current `Value` call, `_type` is assigned the value of `ft`. If verification happens then, `t` would be the

"becoming" is a bit ambiguous here, I first thought it meant ft = _type, then realized one could also read it as _type = ft. Maybe you have an even better way to express it.

Comment on lines +1354 to +1360

// In rare cases, `_type` and `t` have incompatible opinion on speculative type, resulting into a too small intersection
// (such as AnyNull), which is removed in cleanup_speculative. From that `ft` has empty speculative type. After the end
// of the current `Value` call, `ft` (that is returned) is becoming `_type`. If verification happens then, `t` would be the
// same (union of input types), but the new `_type` has now no speculative type, the result of `t->filter_speculative(_type)`
// has the speculative type of `t` (if it's not removed because e.g. the resulting type is exact and non null) and not empty
// (like the previously returned type). In such a case, doing the filtering one time more allows to reach a fixpoint.
Copy link
Contributor

Choose a reason for hiding this comment

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

From that ft has empty speculative type

I'm not very familiar with speculative types. Does "empty speculative" == TOP speculative type? Or rather "no speculative type", which essencially means it is BOTTOM type?

Because then if we filter x with TOP we should still get TOP, but if we filter with BOTTOM we get x. And that would fit better with your statement later on:

but the new _type has now no speculative type, the result of t->filter_speculative(_type) has the speculative type of t

Can you clarify please for my understanding? :)

Copy link
Contributor

Choose a reason for hiding this comment

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

And does cleanup_speculative happen during t->filter_speculative(_type), right?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if a minimal example would help here. I'm thinking something like this:

In rare cases, `_type` and `t` have incompatible opinion on speculative type, resulting into a too small intersection
t: Object (A)
_type: Object (B)
We filter them. Since A and B have no intersection, the speculative type is removed. This means the speculative type is implicitly "Object", and not TOP, as the intersection of A and B would suggest.
ft = t->filter_speculative(_type) = Object

After PhiNode::Value, we assign _type = ft. During verification, we run PhiNode::Value again, but this time:
t: Object (A) // same as above
_type: Object // ft from above
And now we get:
ft = t->filter_speculative(_type) = Object (A)

... argue about fixpoint next ...

It is a first proposal, and a little verbose... maybe you could find something more concise.

Copy link
Member Author

Choose a reason for hiding this comment

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

If we have no speculative type, it means we don't have a guess about what the type could be (more precisely than the actual type). You can say it's bottom, or that it's the same as the non-speculative type. And indeed, if you do t->filter_speculative(_type) when _type has no speculative type, the result has the same speculative type as t except when this one doesn't already pass cleanup_speculative, but then, it's still a fixpoint.

Copy link
Member Author

@marc-chevalier marc-chevalier Nov 19, 2025

Choose a reason for hiding this comment

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

I don't see much value in your "minimal example". Isn't it just naming "A" and "B" instead of saying "speculative type of t" and "speculative type of _type"? I'm not fan of that: that just introduce more symbols, more names that are not in the code.

Also I'm very careful with examples, they are often misleading by giving a feeling that it's exactly how it happens. For instance Since A and B have no intersection is not always true, it just needs the intersection to be above centerline, a small enough intersection. I might rephrase later, but it seems you got what is happening and I first need a decision on the solution before spending time in cleanup that might be entirely erased. It seems that the fixpoint way was far from consensual.

Comment on lines +1370 to +1375
assert(ft == Type::TOP, ""); // Canonical empty value
}

else {

if (jt != ft && jt->base() == ft->base()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Formatting looks a bit funny here.

const Type* first_ft = ft;
ft = t->filter_speculative(first_ft);
#ifdef ASSERT
// The following logic has been moved into TypeOopPtr::filter.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this mean? What logic are you referring to? The one here? But then you say it was moved to TypeOopPtr::filter ...but it is still here? Can you clarify?

Copy link
Contributor

Choose a reason for hiding this comment

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

Or are you saying it is moved "from" rather than "into", i.e. this is some sort of code duplication?

Copy link
Member Author

Choose a reason for hiding this comment

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

This block is a duplication of the existing one, to go after the second filtering as @dafedafe noticed. I have no idea what is the comment for. As I told @dafedafe, if it stays, I'll factor it out, but it is premature cleanup at this point.

#ifdef ASSERT
// The following logic has been moved into TypeOopPtr::filter.
const Type* jt = t->join_speculative(first_ft);
if (jt->empty()) { // Emptied out???
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
if (jt->empty()) { // Emptied out???
if (jt->empty()) {

Comment seems redundant.

* -XX:CompileOnly=compiler.igvn.ClashingSpeculativeTypePhiNode::test1
* -XX:CompileCommand=quiet
* -XX:TypeProfileLevel=222
* -XX:+AlwaysIncrementalInline
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
* -XX:+AlwaysIncrementalInline
* -XX:+AlwaysIncrementalInline

GitHub Actions is giving us this:

Error: VM option 'AlwaysIncrementalInline' is develop and is available only in debug version of VM.
Improperly specified VM option 'AlwaysIncrementalInline'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

Probably this would help: -XX:+IgnoreUnrecognizedVMOptions

@marc-chevalier
Copy link
Member Author

I think reading the code and the comments to understand the situation might not be as good as reading the description of this PR. I regret I gave a reproducer and proposed a solution.

Given the very obvious lack of consensus on the Valhalla PR, it is clear that this issue might evolve radically and that the proposed solution may not be the final one. Therefore, I will not do any cleanup before agreeing on the way to go, as it might very well be erased and it would be a very poor use of everybody's time.

@eme64
Copy link
Contributor

eme64 commented Nov 19, 2025

@marc-chevalier @merykitty @rwestrel I only just realized that the conversation from openjdk/valhalla#1717 still had relevance here. I had a very nice call with Marc, and he graciously explained things to me. I'll summarize my thoughts below.

It seems to be possible that the local speculative type of the phi (_type) and the speculative type of its inputs (t) do not have an intersection that is either null or top. Initially surprising: t->filter_speculative(_type) does not actually produce an intersection of the speculative type in this case, but removes the speculative type in cleanup_speculative because the type is above_centerline. Marc told me that Roland added this, to avoid "over speculation" because the type is now too narrow, and more likely to be wrong.

My question was if it is sane that we get this kind of inconsistent speculation: it must mean that our profiling has delivered somewhat inconsistent results. We would have to dig a bit deeper into the reproducer now, but it seems that an inlined (inner) method found one type during profiling, but the caller (outer method) found another at the phi. It would be interesting to understand a bit more what this implies. I could see these options:

  • We assume both assumptions (speculations are correct). i.e. if there is a path for the type of the inner method to the place of the outer method, then it must be a subtype of what we profiled at both places. And it happens that this is only null or top, so we conclude it is impossible for something non-null to come through this path. It would be reasonable to speculate on this.
  • Maybe profiling is incomplete: we see one type in the inner, and another type in the outer context. So it might be likely that both show up eventually at the phi.
  • One more thought: the more speculative assumptions are "intersected", the more likely this combined speculation is to be incorrect at runtime. Conceptually, each speculation has a probability of failure, and if you apply many of them, the success probability goes lower and lower.

We could now dig deep into if Roland's cleanup_speculative logic that prevents "over speculation" is reasonable, or if we should extend it. But that goes out of scope for this bug fix here, in my opinion.

The question is now what we should do here. There are at least 3 options:

  • The speculative type should be null, the natural intersection of the two speculative types. To me, that sounds like the most consistent solution. But it would require reconsidering the cleanup_speculative logic, a lot of effort. But maybe @merykitty has the desire to tackle such a project?
  • The speculative type should be Object (or the union of the two speculative types). That is the same as removing the speculative type. But currently we don't mark the type as "widened", and so it is possible to later narrow it again once we filter again - so we were not able to reach a fixpoint yet, and that is a problem.
  • Just pick one of the two speculative types. It's an arbitrary choice. But it's not incorrect. Marc's approach of filtering again acheives exactly that: first step removes the speculative type, the second step picks the speculative type of the incoming type. It seems to be the smallest code change, and so that may be best for a relative edge case like this.

There is a bit of a question how much of an edge case this really is. Probably quite. But we also cannot rely on our fuzzers here, as they don't really produce Object Classes at all.

Anyway: I'm the least qualified compared to @merykitty and @rwestrel , but I would vote for Marc's current proposal. Plus the required code cleanups, of course.

@merykitty
Copy link
Member

I'm not sure if I'm correct, but I think speculative types themselves may not be consistent. For example, if they are consistent, then you will expect that the profiled types of the return values of a method a when calling from method b would be a subset of the profiled types of the returned values of a in general. However, this may not be the case, as we can ask for the second information first, then another type is introduced, then suddenly a method seems not to return a type C, but it does seem to return C if calling from b. As a result, maybe we can abandon trying to verify the correctness of speculative type computations.

Additionally, in the test case, the speculative type being empty is correct, the path is speculatively unreachable, maybe we can use that information to cut off the branches, simplify the CFG for better compilation?

@rwestrel
Copy link
Contributor

Additionally, in the test case, the speculative type being empty is correct, the path is speculatively unreachable, maybe we can use that information to cut off the branches, simplify the CFG for better compilation?

Attached is another test case that reproduces the same issue.
TestSpeculativeTypes.java

I run that one with:

$ for i in seq 100; do java -XX:-TieredCompilation -XX:-UseOnStackReplacement -XX:-BackgroundCompilation -XX:CompileOnly=TestSpeculativeTypes::test2 -XX:CompileOnly=TestSpeculativeTypes::inlined3 -XX:CompileCommand=quiet -XX:TypeProfileLevel=200 -XX:+AlwaysIncrementalInline -XX:VerifyIterativeGVN=10 -XX:CompileCommand=dontinline,TestSpeculativeTypes::notInlined1 -XX:+StressIncrementalInlining TestSpeculativeTypes || break; done

It usually fails after a few runs. That one has conflicting profile data but no dead path. What you're suggesting has some risk and unclear benefits so I think would need to be investigated separately.

@eme64
Copy link
Contributor

eme64 commented Nov 25, 2025

@rwestrel @merykitty @marc-chevalier Suggestion:

  • Marc adds Roland's new reproducer.
  • Marc annotates both the existing and Roland/s new reproducer: show where the speculative types come from, and where they flow, and where the Phi with the conflicting speculative type is located.
  • We keep the speculative type verification. Because I'm not convinced that we should just remove verification yet.

In future RFE's we could consider:

  • Removing verification for speculative types. But I wonder if that is smart.
  • Reconsider if we should really do the "widening" if we have above_centerline. As Roland said: there are risks to this. There must have been some historical reason for this. But who knows, maybe it hurts in more cases than it helps. Speculation is always based on a heuristic, and we would need a few benchmarks to show the impact, and we would also need to run some bigger benchmarks to see the impact. That is a lot of effort that would need to happen outside this bugfix. I suppose it is a tradeoff between code size (win if we cut paths) and cost of recompilation (if we fail speculative checks)?

I suppose it depends on what our definition of "consistent" is for speculative types. Of course they can be wrong at runtime, that is the whole point of speculation, so in a sense that makes them "inconsistent". But we could at least enforce some "consistency" in the computation that we do with the speculative types: intersections and unions of types should be done "consistently", I suppose.

I'm trying to reconstruct Quan-Anh's definition of "consistent":

For example, if they are consistent, then you will expect that the profiled types of the return values of a method a when calling from method b would be a subset of the profiled types of the returned values of a in general.

Does this kind of problem not also arise for non-speculative types, at least during CCP or IGVN? First I'm thinking of Casts, but maybe those are special. They can have narrower type than their input type. What about Phi nodes? During IGVN, do we always expect the input types to be a subtype of the output type? I guess so?

Observation: "widening" like that what happens here because of above_centerline and elsewhere like integer types with their "widening" (e.g. widening because loop phi continually grows the range) still means that input types are a subset of the output type.

So according to Quan-Anh's definition, we start with an inconsistent speculative type. But would it not be nice if it eventually became consistent, specifically after CCP/IGVN. And we could already get local consistency after every Value call, right? If yes: it would be nice to have verification for that.

@marc-chevalier
Copy link
Member Author

In future RFE's we could consider:

I might have missed something, but which solution would you then include for now?

@eme64
Copy link
Contributor

eme64 commented Nov 25, 2025

In future RFE's we could consider:

I might have missed something, but which solution would you then include for now?

The one you have now: one more round of filtering to get to fixpoint ;)

@chhagedorn
Copy link
Member

But would it not be nice if it eventually became consistent, specifically after CCP/IGVN.

Just a side node: We remove the speculative types after incremental inlining here:

// Remove the speculative part of types and clean up the graph from
// the extra CastPP nodes whose only purpose is to carry them. Do
// that early so that optimizations are not disrupted by the extra
// CastPP nodes.
remove_speculative_types(igvn);

@marc-chevalier
Copy link
Member Author

Interesting, and indeed, in what I know, we crash inside the inline_incrementally, a few lines above.

@marc-chevalier
Copy link
Member Author

I've added the second test (and I confirm that it crashes quickly on master), simplified a bit, commented, and merge with master.

@marc-chevalier
Copy link
Member Author

I think it would be reasonable to move forward with the current solution, at the moment. It would resolve some existing failures in Valhalla (so it would be nice if it's in soon), it is on the safe side, without putting too much effort in a case that seems quite rare, or in improvements with unclear benefits.

Yet, I agree that is not fundamentally very satisfying, but this fix doesn't prevent (or make harder) a possible future broader revisit of speculative types.

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.

I think this solution is reasonable, and we discussed different options.

Thanks @marc-chevalier for working on this.

And thanks especially for the extra descriptions in the tests. It was super fascinating to see how we get to the inconsistent speculative types :)

@merykitty It would be really interesting to see if the "widening" we do when we go above centerline with speculative types really makes sense. I'm sure there are micro benchmarks that would benefit from the extra speculation, and others that would suffer from it. I suppose we would have to make the changes and run larger benchmarks. Is that something you would want to look into?

Comment on lines +1372 to +1407
#ifdef ASSERT
const Type* ft_ = t->filter_speculative(ft);
if (!Type::equals(ft, ft_)) {
stringStream ss;

ss.print("At node:\n");
this->dump("\n", false, &ss);

for (uint i = 1; i < req(); ++i) {
ss.print("in(%d): ", i);
if (r->in(i) && phase->type(r->in(i)) == Type::CONTROL) {
const Type* ti = phase->type(in(i));
ti->dump_on(&ss);
}
ss.print_cr("");
}

ss.print("t: ");
t->dump_on(&ss);
ss.print_cr("");

ss.print("_type: ");
_type->dump_on(&ss);
ss.print_cr("");

ss.print("Filter once: ");
ft->dump_on(&ss);
ss.print_cr("");
ss.print("Filter twice: ");
ft_->dump_on(&ss);
ss.print_cr("");
tty->print("%s", ss.base());
tty->flush();
assert(false, "computed type would not pass verification");
}
#endif
Copy link
Contributor

Choose a reason for hiding this comment

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

You could consider moving this to a seperate verification method, but up to you.

It is also very verbose. I wonder if it really makes sense to have that much code. But again: up to you.

You could also make your dump less verbose, and then format it directly into the assert. The benefit would be that if a fuzzer finds such a failure we would see more directly what's going on.

Copy link
Member Author

Choose a reason for hiding this comment

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

To be fair, it's not printing as much as it looks like. This is verbose because I cannot write

ss.print_cr("t: %s", t->to_string());

or something like

ss.print_cr("t: %magic", pp_type, t);

for

ss.print("t: ");
t->dump_on(&ss);
ss.print_cr("");

I'm rather tempted to keep it as it is (or close), because that it the kind of information I wanted to see when I was working on that. I suspect if the assert fails and someone has to look at that again, they might find the same information useful as well.

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Nov 27, 2025
@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Nov 27, 2025
Copy link
Contributor

@rwestrel rwestrel left a comment

Choose a reason for hiding this comment

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

otherwise looks reasonable to me.

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Dec 15, 2025
@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Dec 16, 2025
Copy link
Contributor

@rwestrel rwestrel 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 making the change. Looks good to me.

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Dec 16, 2025
@marc-chevalier
Copy link
Member Author

/integrate

Thanks everyone for the reviews.

@openjdk
Copy link

openjdk bot commented Dec 16, 2025

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

openjdk bot commented Dec 16, 2025

@marc-chevalier Pushed as commit 76e79db.

💡 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 hotspot-compiler-dev@openjdk.org integrated Pull request has been integrated

Development

Successfully merging this pull request may close these issues.

6 participants