-
Notifications
You must be signed in to change notification settings - Fork 6.2k
8350579: Remove Template Assertion Predicates belonging to a loop once it is folded away #23823
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
loop once it is folded away during IGVN
|
👋 Welcome back chagedorn! A progress list of the required criteria for merging this PR into |
|
@chhagedorn 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 no new commits pushed to the ➡️ To integrate this PR with the above commit message to the |
|
@chhagedorn 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. |
|
That looks reasonable to me but imaking sure that predicates in the process of being removed are properly stepped over feels like something that could be fragile. So I'm wondering if there would be a way to mark predicates as being for a particular loop (maybe storing the loop's node id they apply to in predicate nodes and making sure it's properly updated as loops are cloned etc.) so when there is a mismatch between the loop and predicate it can be detected? |
|
Thanks Roland for having a look. I agree that it is indeed quite fragile. It started out with a quite simple fix but then I found more and more cases with fuzzing where we have some weird in-between states in IGVN while a predicate is being folded where matching failed. I was not super happy with matching predicates during IGVN which is difficult and error-prone to get right.
That's an interesting idea that could work more reliably. Let me think about that more. |
eme64
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.
Generally looks good, I have a few suggestions and questions :)
|
|
||
| // The visitor visits all Template Assertion Predicates and kills them by marking them useless. They will be removed | ||
| // during next round of IGVN. | ||
| class KillTemplateAssertionPredicates : public PredicateVisitor { |
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.
| class KillTemplateAssertionPredicates : public PredicateVisitor { | |
| class KillTemplateAssertionPredicateVisitor : public PredicateVisitor { |
src/hotspot/share/opto/loopnode.cpp
Outdated
| KillTemplateAssertionPredicates kill_template_assertion_predicates(phase->is_IterGVN()); | ||
| PredicateIterator predicate_iterator(skip_strip_mined()->in(EntryControl)); | ||
| predicate_iterator.for_each(kill_template_assertion_predicates); |
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.
| KillTemplateAssertionPredicates kill_template_assertion_predicates(phase->is_IterGVN()); | |
| PredicateIterator predicate_iterator(skip_strip_mined()->in(EntryControl)); | |
| predicate_iterator.for_each(kill_template_assertion_predicates); | |
| KillTemplateAssertionPredicateVisitor kill_template_assertion_predicate_visitor(phase->is_IterGVN()); | |
| PredicateIterator predicate_iterator(skip_strip_mined()->in(EntryControl)); | |
| predicate_iterator.for_each(kill_template_assertion_predicate_visitor); |
Nit:
KillTemplateAssertionPredicateVisitor might be nicer because it tells me from the beginning that it is a visitor.
KillTemplateAssertionPredicates had me thinking that is some kind of constructor that goes ahead and kills things already. The plural "predicates" also indicated that it would do that for all predicates.
| return false; | ||
| } | ||
| return has_assertion_predicate_opaque(maybe_success_proj) && has_halt(maybe_success_proj); | ||
| return has_assertion_predicate_opaque_or_con_input(maybe_success_proj); |
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.
Quick control question: Could skipping over constants also mean that we traverse up too far at some point? Like skipping not not just the predicates but also an unrelated check that is about to constant fold? I suppose that should not really create issues?
| // during next round of IGVN. | ||
| class KillTemplateAssertionPredicates : public PredicateVisitor { | ||
| PhaseIterGVN* _igvn; | ||
| public: |
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.
| public: | |
| public: |
| /* | ||
| * @test id=NoFlags | ||
| * @bug 8288981 8350579 | ||
| * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+AbortVMOnCompilationFailure |
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.
Can you explain why you are enabling AbortVMOnCompilationFailure?
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 added an additional comment further up in the test to explain the reason.
| // Runs most of the tests except the really time-consuming ones. | ||
| static void runAllTests() { |
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.
Sounds like a bit of a contradiction 😅
runAllTests -> runAllFastTests?
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.
Which ones are the really time-consuming ones? And why do you not run them here?
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 removed the comment for now - we run all tests here. This only applied to the full test added with JDK-8350577 from which I extracted these bits. I can revisit this comment and method name there again :-)
|
Thanks Emanuel for your review! Forgot to move this to draft state. As Roland has pointed out, it is quite fragile to do a matching during IGVN where you need to handle all kinds of of dying predicate shapes. I'm currently moving to a non-IGVN solution (#23941 is a first step). I will then update this patch. The problem to fix is still the same though, just with a different solution. I will get to this once I have integrated some preparatory changes. |
|
@chhagedorn this pull request can not be integrated into git checkout JDK-8350579
git fetch https://git.openjdk.org/jdk.git master
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge master"
git push |
… of IGVN by storing a reference to the loop it originally was created for.
| // Search the Assertion Predicates added by loop predication and/or range check elimination and update them according | ||
| // to the new stride. | ||
| void PhaseIdealLoop::update_main_loop_assertion_predicates(CountedLoopNode* main_loop_head) { | ||
| Node* init = main_loop_head->init_trip(); |
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.
unused
| loop->record_for_igvn(); | ||
| loop_head->clear_strip_mined(); | ||
|
|
||
| update_main_loop_assertion_predicates(clone_head, stride_con); |
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.
Moved down here (see PR description).
|
|
||
| // This class is used to replace the input to OpaqueLoopStrideNode with a new node while leaving the other nodes | ||
| // unchanged. | ||
| class ReplaceOpaqueStrideInput : public BFSActions { |
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.
Moved this up because we now call it from TemplateAssertionPredicate and not TempalteAssertionPredicateExpression.
| TemplateAssertionExpression expression(opaque_node()); | ||
| expression.replace_opaque_stride_input(new_stride, igvn); |
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.
Was just an indirection which I removed which makes it easier to use.
|
|
||
| // Clone this Template Assertion Predicate without modifying any OpaqueLoop*Node inputs. | ||
| TemplateAssertionPredicate TemplateAssertionPredicate::clone(Node* new_control, PhaseIdealLoop* phase) const { | ||
| TemplateAssertionPredicate TemplateAssertionPredicate::clone(Node* new_control, CountedLoopNode* new_loop_node, |
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.
Most changes in this file: Changing phase -> _phase and piping the new CountedLoopNode through the code such that we can initialized the new OpaqueTemplateAssertionPredicate nodes with them accordingly.
| OpaqueTemplateAssertionPredicateNode* opaque_clone = | ||
| new OpaqueTemplateAssertionPredicateNode(bool_into_opaque_node_clone, new_loop_node); | ||
| _phase->C->add_template_assertion_predicate_opaque(opaque_clone); | ||
| _phase->register_new_node(opaque_clone, new_control); |
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.
We don't clone the OpaqueTemplateAssertionPredicateNode anymore with DataNodeGraph::clone_with_opaque_loop_transform_strategy but directly here. This allows us to easily set the new_loop_node for it.
| void ClonePredicateToTargetLoop::clone_template_assertion_predicate( | ||
| const TemplateAssertionPredicate& template_assertion_predicate) { | ||
| TemplateAssertionPredicate cloned_template_assertion_predicate = | ||
| template_assertion_predicate.clone(_old_target_loop_entry, _target_loop_head->as_CountedLoop(), _phase); |
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.
Moved method from .hpp file here because of incomplete CountedLoopNode when calling as_CountedLoop().
| return; | ||
| } | ||
| replace_opaque_stride_input(template_assertion_predicate); | ||
| template_assertion_predicate.update_associated_loop_node(_loop_node); |
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.
We don't need to clone the Template Assertion Expression and hence we only need to update the loop node.
rwestrel
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 to me.
|
Thanks Roland for your review! |
eme64
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 @chhagedorn !
| int unrolled_stride_con = main_loop_head->stride_con() * 2; | ||
| int unrolled_stride_con = stride_con_before_unroll * 2; |
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.
Could we assert that stride_con_before_unroll == main_loop_head->stride_con()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If not, could we assert something similar?
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 thought about somehow asserting here that as well. But the problem is that at this point, we already concatenated the original and the new loop together to represent one round of unrolling. So, we do not find the original loop exit check anymore from which we could have read the stride. That's why I explicitly take the cached stride_con_before_unroll and double it here.
We could have maybe cached the original loop exit node somehow to query it. But I don't think it adds much value since it's as good the original stride which was read from the loop exit node.
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.
Makes sense. The assert is not that important here.
| if (!_loop_node->is_CountedLoop()) { | ||
| // Template Assertion Predicate above LoopNode? Must be unrelated because they can only be created above | ||
| // CountedLoopNodes during Loop Predication or Range Check Elimination. | ||
| return; | ||
| } | ||
|
|
||
| mark_template_useful_if_matching_loop(template_assertion_predicate); | ||
| } | ||
|
|
||
| // If the stored loop node does not match the current loop node from which we iterate from, we found a Template | ||
| // Assertion Predicate belonging to an already earlier folded loop in the graph. We need to drop this Template | ||
| // Assertion Predicate because we are no longer splitting a loop which it belongs to. Moreover, if we do not remove | ||
| // this Template Assertion Predicate, we could wrongly be creating Initialized Assertion Predicates from it at the | ||
| // new loop which has completely unrelated loop values. These Initialized Assertion Predicates can then fail at | ||
| // runtime, and we crash by executing a halt instruction. | ||
| void mark_template_useful_if_matching_loop(const TemplateAssertionPredicate& template_assertion_predicate) const { | ||
| OpaqueTemplateAssertionPredicateNode* opaque_node = template_assertion_predicate.opaque_node(); | ||
| if (opaque_node->loop_node() == _loop_node) { | ||
| template_assertion_predicate.opaque_node()->mark_useful(); | ||
| } | ||
| } |
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'm not sure if it makes to split this into two methods, but that's subjective 😅
It seems to me that the code in visit is an optimization for what happens in mark_template_useful_if_matching_loop, and does not really make sense on its own.
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.
The reasons I've split it is the following:
- The bailout for non-counted loops is actually separate to the marking. So I have a two-step algorithm: bailout + marking which can nicely be split.
- Having
mark_template_useful_if_matching_loop()allows me to quickly readvisit()and understand what's going on. Additionally, I can put the details about why we do the marking at the method comment for more interested code readers. Without the extracted method, I would probably need to put an extra "mark template useful if matching loop" comment + the 6 lines of comments atmark_template_useful_if_matching_loop()into thevisit()method which makes it harder to grasp.
I would prefer to stick to what I have now - but I admit it's a subjective matter :-)
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 leave it up to you :)
But could opaque_node->loop_node() == _loop_node even be true if we have !_loop_node->is_CountedLoop()? or do we actually need a CountedLoop to even have the match?
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.
No, it cannot be true. opaque_node->loop_node() is a CountedLoop. We could have only opaque_node->loop_node() == _loop_node. I think I first had that as an assertion in place but then turned it into a bailout. But you're right, it would already be covered by the check. Given it's a rare edge case, I guess we can get rid of it. Then the reason above does not apply anymore that we have 2 steps but only 1. Then it does not make sense to split it further. I merged it back together.
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.
Thanks Emanuel for your review and comments!
| if (!_loop_node->is_CountedLoop()) { | ||
| // Template Assertion Predicate above LoopNode? Must be unrelated because they can only be created above | ||
| // CountedLoopNodes during Loop Predication or Range Check Elimination. | ||
| return; | ||
| } | ||
|
|
||
| mark_template_useful_if_matching_loop(template_assertion_predicate); | ||
| } | ||
|
|
||
| // If the stored loop node does not match the current loop node from which we iterate from, we found a Template | ||
| // Assertion Predicate belonging to an already earlier folded loop in the graph. We need to drop this Template | ||
| // Assertion Predicate because we are no longer splitting a loop which it belongs to. Moreover, if we do not remove | ||
| // this Template Assertion Predicate, we could wrongly be creating Initialized Assertion Predicates from it at the | ||
| // new loop which has completely unrelated loop values. These Initialized Assertion Predicates can then fail at | ||
| // runtime, and we crash by executing a halt instruction. | ||
| void mark_template_useful_if_matching_loop(const TemplateAssertionPredicate& template_assertion_predicate) const { | ||
| OpaqueTemplateAssertionPredicateNode* opaque_node = template_assertion_predicate.opaque_node(); | ||
| if (opaque_node->loop_node() == _loop_node) { | ||
| template_assertion_predicate.opaque_node()->mark_useful(); | ||
| } | ||
| } |
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.
The reasons I've split it is the following:
- The bailout for non-counted loops is actually separate to the marking. So I have a two-step algorithm: bailout + marking which can nicely be split.
- Having
mark_template_useful_if_matching_loop()allows me to quickly readvisit()and understand what's going on. Additionally, I can put the details about why we do the marking at the method comment for more interested code readers. Without the extracted method, I would probably need to put an extra "mark template useful if matching loop" comment + the 6 lines of comments atmark_template_useful_if_matching_loop()into thevisit()method which makes it harder to grasp.
I would prefer to stick to what I have now - but I admit it's a subjective matter :-)
| int unrolled_stride_con = main_loop_head->stride_con() * 2; | ||
| int unrolled_stride_con = stride_con_before_unroll * 2; |
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 thought about somehow asserting here that as well. But the problem is that at this point, we already concatenated the original and the new loop together to represent one round of unrolling. So, we do not find the original loop exit check anymore from which we could have read the stride. That's why I explicitly take the cached stride_con_before_unroll and double it here.
We could have maybe cached the original loop exit node somehow to query it. But I don't think it adds much value since it's as good the original stride which was read from the loop exit node.
eme64
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.
Still approved. My comments were just suggestions / control questions. Thanks for answering 😊
|
Thanks Emanuel for your review! I will submit some more testing with the latest changes before integration. |
|
Testing looked good. /integrate |
|
Going to push as commit c953e0e.
Your commit was automatically rebased without conflicts. |
|
@chhagedorn Pushed as commit c953e0e. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
The patch fixes the issue of creating an Initialized Assertion Predicate at a loop X from a Template Assertion Predicate that was originally created for a loop Y. Using the unrelated loop values from loop Y for the Initialized Assertion Predicate will let it fail during runtime and we execute a
haltinstruction. This was originally reported with JDK-8305428.Note that most of the line changes are from new tests.
The Problem
There are multiple test cases triggering the same problem. In the following, when referring to "the test case", I'm referring to
testTemplateAssertionPredicateNotRemovedHalt()which was written from scratch and contains more detailed comments explaining how we end up with executing aHaltnode in more details.An Inner Loop without Parse Predicates
The graph in
testTemplateAssertionPredicateNotRemovedHalt()looks like this after creatingLoopNodesfor the outerforand innerwhile (true)loop:We only have Parse Predicates for the outer loop. Why?
Before beautify loop, we have the following region which merges multiple backedges - the one from the
forloop and the one from thewhile (true)loop:In
IdealLoopTree::merge_many_backedges(), we notice that the hottest backedge is hot enough such that it is worth to have a separate merge point region for the inner and outer loop. We set everything up and eventually inIdealLoopTree::split_outer_loop(), we create a secondLoopNode.For this inner
LoopNode, we cannot set upParse Predicateswith the same UCTs as used for the outer loop. It would be incorrect when taking the trap to re-execute the inner and outer loop again while having already executed some of the outer loop's iterations. Thus, we get the graph shape with back-to-backLoopNodesas shown above.Predicates from a Folded Loop End up at Another Loop
As described in the previous section, we have an inner and outer
LoopNodewhile the inner does not have Parse Predicates. In a series of events (see test case comments for more details), we first hoist a range check out of the outer loop during Loop Predication with a Template Assertion Predicate. Then, we fold the outer loop away because we find that it is only running for a single iteration and the backedge is never taken. The Template Assertion Predicate together with the Parse Predicates end up at the inner loop running fromi = 80:Creating Initialized Assertion Predicate with Wrong Loop Values
We now split the inner loop by creating pre-main-post loops. In this process, we create new Template Assertion Predicates with the new init value of the main and post loop. We also create Initialized Assertion Predicates from the new templates. But these now use the init value from the inner loop, even though the Assertion Predicates were created with the loop values from the outer loop:
iArrShorthas only a size of10but512 Phitakes value80. During runtime, this Initialized Assertion Predicate fails and we crash by executing a halt instruction.New Update from March 19, 2025:
New Proposed Solution
Why Matching During IGVN Is Difficult and Fragile
After some discussion with @rwestrel (also see comments below), we've decided to not do any predicate matching during IGVN. It is quite fragile and error prone since you need to handle all kinds of different dying predicate shapes. Sometimes it's even impossible to tell for sure if a dying node is a predicate or just an unrelated node. When in doubt, we need to assume it is a predicate because when we wrongly stop predicate iteration, we could miss to update some other predicates which could lead to failures. On the other hand, we might find other unrelated predicates by wrongly treating non-predicate nodes as predicates.
Removing Predicates during
PhaseIdealLoop::eliminate_useless_predicates().Instead of removing no longer needed Template Assertion Predicates during IGVN, we choose a different approach. With the preparation work done with #24013 and #23941, we can now remove Template Assertion Predicates created for an already folded
CountedLoopduringPhaseIdealLoop::eliminate_useless_predicates().Each
OpaqueTemplateAssertionPredicatenode that is associated with Template Assertion Predicate now maintains a reference to itsCountedLoop. During loop splitting, theCountedLoopis updated accordingly. For example, when unrolling a loop, the cloned loop will be the new loop head and thus needs to be updated as such in eachOpaqueTemplateAssertionPredicate. These updates are implemented in the methods to clone/update Template Assertion Predicates.Algorithm
When visiting all predicates from all loop in
PhaseIdealLoop::eliminate_useless_predicates(), we also check if the loop node, from which we started the predicate iteration, matches the loop node stored in theOpaqueTemplateAssertionPredicatenodes. If not, then we do not mark them useless. This allows us to remove the unrelatedOpaqueTemplateAssertionPredicatenodes whose loop nodes were removed during IGVN.As additional verification, I want to check that all now useless
OpaqueTemplateAssertionPredicatenodes do not contain any references toCountedLoopnodes that are still in the graph. However, that does not reliably work before having the full fix with JDK-8350577. I therefore filed JDK-8352418 to keep track of that and follow up with the verification code after JDK-8350577 is integrated.Additional Updates
PhaseIdealLoop::update_main_loop_assertion_predicates()down to after the loop cloning because we need to have a reference to the clonedCountedLoopnode that becomes the new loop head. I adjusted the involved code accordingly.OpaqueTemplateAssertionPredicateNode::dump_spec()for debugging purposes.I added some PR comments in the code for additional help.
Outdated previous solution - left here to preserve history
Old Proposed Solution
We should remove any Template Assertion Predicate when a
CountedLoopNodeis folded away. This is implemented inCountedLoopNode::Ideal()to do that right during IGVN when a loop node is folded. This ensures that we do not miss any dying loop.Implementation Details
KillTemplateAssertionPredicatesvisitor to do that. This required a newTemplateAssertionPredicate::kill_during_igvn()method to directly operate onPhaseIterGVNinstead ofPhaseIdealLoop.Ifnodes with some specific inputs (i.e. flavors ofOpaque*nodes) or outputs (i.e.Haltor UCTs). Since we now usePredicateIteratorduring IGVN, we need to be more careful when a Regular Predicate is being folded away to still recognize it as a Regular Predicate. When we fail to do so, we could stop the iteration and miss predicates above. The existing checks are not strong enough and required the following tweaks for some situations:Ifhas aConIas input because theOpaque*node was already folded.Haltnode on the false path (done withAssertionPredicate::may_be_assertion_predicate_if()).Ifalready lost one of its output.Ifalways has two outputs (done withRuntimePredicate::is_being_folded_without_uncommon_proj()andAssertionPredicate::may_be_assertion_predicate_if()).Tests
Thanks,
Christian
Progress
Issue
Reviewers
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/23823/head:pull/23823$ git checkout pull/23823Update a local copy of the PR:
$ git checkout pull/23823$ git pull https://git.openjdk.org/jdk.git pull/23823/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 23823View PR using the GUI difftool:
$ git pr show -t 23823Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/23823.diff
Using Webrev
Link to Webrev Comment