-
Notifications
You must be signed in to change notification settings - Fork 6.1k
8347901: C2 should remove unused leaf / pure runtime calls #25760
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
8347901: C2 should remove unused leaf / pure runtime calls #25760
Conversation
|
👋 Welcome back mchevalier! A progress list of the required criteria for merging this PR into |
|
@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: 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 490 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. ➡️ To integrate this PR with the above commit message to the |
|
@marc-chevalier The following labels 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 lists. If you would like to change these labels, use the /label pull request command. |
Webrevs
|
|
Just a side comment: "Integer division is not pure as dividing by zero is throwing." is only true for some platforms. See JDK-8299857. |
|
Right. Yet, it's safe to consider it as non-pure, as an over-approximation, but it could be refined. |
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.
@marc-chevalier Nice work!
I have a first set of qestions / suggestions :)
| if (ctrl->Opcode() == Op_Tuple) { | ||
| // Jumping over Tuples: the i-th projection of a Tuple is the i-th input of the Tuple. | ||
| ctrl = ctrl->in(_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.
Do you need to special-case this here? Why does the ProjNode::Identity not suffice? Are there potentially other locations where we now would need this special logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a good question. That is something I picked from Vladimir's implementation and it seemed legitimate. But now you say it, is it needed? Not sure. I'm trying to find that out. Would ::Identity be enough? It's tempting to say so, right! I'd say it can be not enough if we need adr_type before idealizing the ProjNode (no idea if that happens). Is there any other places to adapt? One could think so, but actually, I can't find such an example. Other methods of ProjNode for instance rely on the type of the input (which is correctly handled in TupleNode), and so should already work fine.
I'm trying to understand what happens if we don't have that. But maybe @iwanowww would have some helpful insight?
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 don't remember all the details now, but there were some problems when ProjNode::adr_type() encounters TupleNode.
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've run some test without this special handling and I couldn't see nothing more failing. Maybe it was more necessary in Vladimir's original usecase (reachability). In my case, maybe I don't need adr_type to handle nicely TupleNodes, but let's not set a trap for the future.
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.
@marc-chevalier Thanks for addressing my comments! I now have a few more for you :)
src/hotspot/share/opto/callnode.cpp
Outdated
| } | ||
|
|
||
| Node* CallLeafPureNode::Ideal(PhaseGVN* phase, bool can_reshape) { | ||
| if (is_dead()) { // dead 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.
| if (is_dead()) { // dead node | |
| if (is_dead()) { |
The comment seemed redundant. You could say who else is responsible of cleaning up the dead node though.
What would happen if the CallLeafPureNode loses its control projection, but not the other uses? I don't even know if that is possible. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IGVN takes care of removing a node without output.
This was motivated by https://bugs.openjdk.org/browse/JDK-8353341 I think @TobiHartmann told me it's common not to touch dead nodes during idealization.
I think it is possible to lose control projection but not data because data was already found dead, but data wasn't yet (but should happen shortly after). As soon as the data projection disappear, the node should be removed. remove_dead_node (used in IGVN) is aggressively removing a dead node and all the usages that become recursively dead. It's the classic constraint of having to find that both data and control are top when one is.
| #endif | ||
| }; | ||
|
|
||
| class CallLeafPureNode : public CallLeafNode { |
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.
You need a short description about what this node is for. What are the assumptions about it?
src/hotspot/share/opto/divnode.cpp
Outdated
| if (!can_reshape) { | ||
| return nullptr; | ||
| } |
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.
Would this prevent us from doing the make_tuple_of_input_state_and_top_return_values trick? Because it seems to me that we do not need to reshape the node for that, right? Maybe you should reorder things for that?
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.
Also, you should probably call CallLeafPureNode::Ideal instead of duplicating its logic here and in other subclasses.
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.
In an earlier issue (JDK-8349523), I tried to remove these nodes during parsing. It didn't work well. The problem is that it's transformed by GVN before setting any output projection, so of course, the node is removing itself before having the opportunity of being used. We wait until usages have been set before we try to remove the node (or replace it with a tuple).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like a bug at the use-site... don't you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, it's relatively normal. Adding a comment to explain.
| Node* Parse::floating_point_mod(Node* a, Node* b, BasicType type) { | ||
| assert(type == BasicType::T_FLOAT || type == BasicType::T_DOUBLE, "only float and double are floating points"); | ||
| CallNode* mod = type == BasicType::T_DOUBLE ? static_cast<CallNode*>(new ModDNode(C, a, b)) : new ModFNode(C, a, b); | ||
| CallLeafPureNode* mod = type == BasicType::T_DOUBLE ? static_cast<CallLeafPureNode*>(new ModDNode(C, a, b)) : new ModFNode(C, a, b); |
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.
May I ask why we only need the static_cast for the ModDNode 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.
It's C/C++ being annoying here: both branches of the ternary must have the same type, or something compatible. If I remove the cast:
error: conditional expression between distinct pointer types 'ModDNode*' and 'ModFNode*' lacks a cast
With the case, C++ can converrt the ModFNode* into a CallLeafPureNode* just fine.
I didn't invent the cast, it was here before, but good to question it.
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 for digging into this Marc. The changes look good to me. I just added a few minor comments / questions.
this PR doesn't propose a way to move pure calls around
Should we have a separate RFE for that?
src/hotspot/share/opto/divnode.cpp
Outdated
| Node* super = CallLeafPureNode::Ideal(phase, can_reshape); | ||
| if (super != nullptr) { | ||
| return super; |
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't we just do return CallLeafPureNode::Ideal(phase, can_reshape); at the end of ModFNode::Ideal instead of return nullptr? That's what we usually do in C2, for example in CallStaticJavaNode::Ideal -> CallNode::Ideal. Feels more natural to me and would avoid the super != nullptr check. Also for the other Ideal methods that you modified.
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 could but it's not that direct. ModFNode::Ideal has 6 returns (without mine):
- 3 are
return replace_with_con(...);which in their turn returnnullptrbut after making changes in the graph. - 2 are
return nullptr; - 1 is actually returning a node.
And especially the final one is
return replace_with_con(igvn, TypeF::make(jfloat_cast(xr)));If we change replace_with_con to actually return a TupleNode to do the job, we still have 2 places where to call the base class' Ideal. So I'm not sure how much better it would be to duplicate the call. It also adds a maintenance burden: if one adds another case where we don't want to make changes, one needs to add another call to CallLeafPureNode::Ideal. I think it's because of the structure of this function: rather than selecting cases where we want to do something and reaching the end with only the leftover cases, we select the cases we don't want to continue with, and we return early, making more cases where we should call the super method.
I'll try something.
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.
Ah, makes sense. Feel free to leave as-is then.
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.
There, you can see the thing I've tried. It changes a bit more code, but overall, I think it makes it clearer and address your comment.
| bool result_is_unused = proj_out_or_null(TypeFunc::Parms) == nullptr; | ||
| bool not_dead = proj_out_or_null(TypeFunc::Control) != nullptr; | ||
| if (result_is_unused && not_dead) { | ||
| return replace_with_con(igvn, TypeF::make(0.)); |
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 we replace all the other usages of ModFloatingNode::replace_with_con by TupleNode and get rid of that method?
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.
Yes, that sounds like a good idea. One still need to build the right TupleNode, which takes a bit of code. So in detail, I'd rather replace replace_with_con with a function returning the right TupleNode to be as concise on the call-site.
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.
Yes, that sounds good.
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 for making these changes, I like that version more.
|
@iwanowww would you like to take a look at it, since you have quite some context already? |
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.
|
/integrate Thanks all for your comments! |
|
Going to push as commit ed70910.
Your commit was automatically rebased without conflicts. |
|
@marc-chevalier Pushed as commit ed70910. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
A first part toward a better support of pure functions, but this time, with guidance from @iwanowww.
Pure Functions
Pure functions (considered here) are functions that have no side effects, no effect on the control flow (no exception or such), cannot deopt etc.. It's really a function that you can execute anywhere, with whichever arguments without effect other than wasting time. Integer division is not pure as dividing by zero is throwing. But many floating point functions will just return
NaNor+/-infinityin problematic cases.Scope
We are not going all powerful for now! It's mostly about identifying some pure functions and being able to remove them if the result is unused. Some other things are not part of this PR, on purpose. Especially, this PR doesn't propose a way to move pure calls around. The reason is that pure calls are later expanded into regular calls, which require a control input. To be able to do the expansion, we just keep the control in the pure call as well.
Implementation Overview
We created here some new node kind for pure calls, inheriting leaf calls, that are expanded into regular leaf calls during final graph reshaping. The possibility to support pure call directly in AD file is left open.
This PR also introduces
TupleNode(largely based on an original idea/implem of @iwanowww), that just tie multiple input together and play well withProjNode: the n-th projection of aTupleNodeis the n-th input of the tuple. This is a convenient way to skip and remove nodes from the graph while delegating the difficulty of the surgery to the trusted IGVN's implementation.Thanks,
Marc
Progress
Issue
Reviewers
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/25760/head:pull/25760$ git checkout pull/25760Update a local copy of the PR:
$ git checkout pull/25760$ git pull https://git.openjdk.org/jdk.git pull/25760/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 25760View PR using the GUI difftool:
$ git pr show -t 25760Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/25760.diff
Using Webrev
Link to Webrev Comment