-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
JIT: Make BasicBlock jump target private #93152
Conversation
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch Issue DetailsNext step for #93020. CC @dotnet/jit-contrib.
|
|
||
BasicBlock* GetJumpDest() const | ||
{ | ||
return bbJumpDest; |
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 wanted to take these new methods as an opportunity to assert we are accessing the jump target correctly. For example, we should only read bbJumpDest if bbJumpKind is valid, and bbJumpDest should not be null for such blocks (and if we can assert bbJumpDest is only accessed when bbJumpKind is valid, then perhaps there's no need to set bbJumpDest to null when setting bbJumpKind to something like BBJ_NONE). However, these asserts turned out to be overly aggressive -- we seem to have some edge cases in a couple of phases that break these assumptions. For one, we do set and compare bbJumpDest to null in a few places, and checking if the jump kind is valid instead of null-checking bbJumpDest would probably hurt TP (as we would have to compare bbJumpKind to multiple possible valid values, versus just one null check). Would it be worth trying to strengthen our bbJumpKind/bbJumpDest abstraction if it comes at the cost of TP?
|
||
BasicBlock* GetJumpDest() const | ||
{ | ||
return bbJumpDest; |
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 a next step, this could have an assert for those kinds where bbJumpDest is legal:
assert(KindIs(BBJ_CALLFINALLY,BBJ_ALWAYS,BBJ_EHCATCHRET,BBJ_LEAVE,BBJ_COND));
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 wanted to include that too, but this proved to be overly aggressive in a few spots. For example, in Compiler::fgFindBlockILOffset(), we check curr's bbJumpDest as long as it's not a switch statement. I could check if curr's jump kind is valid first to satisfy the assert in GetJumpDest(), but I'm not sure what our stance is on reducing TP to satisfy this constraint.
|
||
void SetJumpDest(BasicBlock* jumpDest) | ||
{ | ||
bbJumpDest = jumpDest; |
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.
maybe the same assert here, which would assume the bbJumpKind is set before changing/setting the bbJumpDest.
|
||
bool JumpsTo(const BasicBlock* jumpDest) const | ||
{ | ||
return (bbJumpDest == jumpDest); |
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.
Same assert here.
I wonder if we should treat switch and non-switch more uniformly. i.e., JumpsTo
might imply any flow graph control transfer, including switches. Maybe we'll evolve this over time.
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.
Asserting bbJumpKind is valid proved to be too aggressive in JumpsTo(), as well. Should I try enabling these asserts in a follow-up PR? I think that refactor will be nontrivial, since we null-check bbJumpDest for non-jump blocks in a bunch of places.
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.
A follow-up PR to add asserts is fine.
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 seems odd that SetJumpKind
has a Compiler*
arg but SetJumpKindAndTarget
doesn't. I would expect the latter to be a logical superset.
If you added a Compiler*
arg you could then fix the switch version to allocate the BBswtDesc
automatically, perhaps.
|
||
void SetJumpSwt(BBswtDesc* jumpSwt) | ||
{ | ||
bbJumpSwt = jumpSwt; |
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 assert(KindIs(BBJ_SWITCH));
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 thought about this, but we have several instances where we set bbJumpSwt before updating bbJumpKind, and I wouldn't want to enforce an order of the function calls -- though maybe that means we should have a function for converting the block to BBJ_SWITCH that updates bbJumpKind and bbJumpSwt simultaneously?
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 should have a function for converting the block to BBJ_SWITCH that updates bbJumpKind and bbJumpSwt simultaneously?
Maybe an overloaded:
SetKindAndTarget(BBjumpKinds kind, BasicBlock* target) => set bbJumpKind/bbJumpDest
SetKindAndTarget(BBjumpKinds kind, BBswtDesc* target) => set bbJumpKind/bbJumpSwt
for those cases where both are being set?
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.
Sure thing, I'll add that in.
/* The following union describes the jump target(s) of this block */ | ||
union { | ||
unsigned bbJumpOffs; // PC offset (temporary only) | ||
BasicBlock* bbJumpDest; // basic block |
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 could imagine that BBJ_COND
could have, here, struct { BasicBlock* bbJumpTrue; BasicBlock* bbJumpFalse; }
where the true case would currently map to bbJumpDest
and the false case to bbNext
. And have accessors for each. Maybe that's where this will evolve?
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 think that's Andy's plan. He mentions in #93020 adding an explicit fall-through successor pointer to BasicBlock that would apply to BBJ_ALWAYS, BBJ_COND, etc. Would it be worth including that work in this PR, or save it for the next one? That change will probably introduce some TP diffs...
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.
Let's hold off... where I'd really like to get to is that these references are either:
- some kind of BasicBlockReference holder struct (so reassignment updates ref counts automagically); or even better, perhaps:
- FlowEdge(s)
The appeal of BasicBlockReference is that someday perhaps I'd like to be able to enumerate all the things that refer a block, including "external" BasicBlock references as well (references from the EH table, from SSA defs, special block references from the compiler object (fgFirstBB, etc), etc).
We could then eliminate all the fussing about with bbRefCounts and either have them maintained automatically or recomputed when needed.
The appeal of FlowEdges is that it simplifies some of the forward likelihood analysis, currently to find an edge to a successor you need to find the successor block and then enumerate the predecessor edges.
But not sure I want to do any of this just yet.
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.
While we are discussing successor edges, I'd like to bring up the fact there is duplication between IR control flow nodes such as SWITCH
/JTRUE
and the block kinds. In some systems, information about successors is determined purely from the IR and "terminator" nodes.
It is an appealing system from the orthogonality/elegance perspective since there is no duplication, but it is unlikely to be suitable for us due to the throughput cost (of at least two additional indirections in HIR).
src/coreclr/jit/assertionprop.cpp
Outdated
@@ -5260,7 +5260,7 @@ class AssertionPropFlowCallback | |||
{ | |||
ASSERT_TP pAssertionOut; | |||
|
|||
if (predBlock->KindIs(BBJ_COND) && (predBlock->bbJumpDest == block)) | |||
if (predBlock->KindIs(BBJ_COND) && predBlock->JumpsTo(block)) |
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.
At first glace i wasn't certain what JumpsTo(block)
was doing, it could be getting or setting, reading the original made it clear. Consider using IsJumpTo
which would make it clearer.
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 would object to IsJumpsTo()
-- that just doesn't read well. If consensus is that JumpsTo()
is confusing, I'd recommend leaving these cases as block->JumpTarget() == block2
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 would actually prefer something like HasJumpTo
since JumpsTo
to me reads like there's no other option (that is, BBJ_ALWAYS) and the "has" makes it clearer this is a predicate.
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.
Either HasJumpTo
or JumpTarget() == block2
sound ok to me.
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 changed JumpsTo
to HasJumpTo
, and kept JumpsToNext
the same since it indicates whether a block will always "jump" to bbNext regardless of its condition.
So asserting |
I like to see a PR with these sorts of mechanical changes without changing behavior, and then see behavioral changes as separate follow-ups. There are so many diffs here with renaming that it is easy to miss something that changes things more fundamentally. In practice you may find you need to do both at the same time to figure out which mechanical changes you want, but you can either keep separate commits locally and PR them one by one, or (more painful) split a commit up to defer the behavior changing parts. |
@AndyAyersMS @BruceForstall PTAL. Per Bruce's suggestion, I added a helper method for simultaneously updating the jump kind and target, and asserting that the jump kind argument is valid (BBJ_ALWAYS, BBJ_COND, etc) doesn't seem to break any of our current invariants. Since we probably want to ensure I imagine checking if the jump target is null is cheaper (in terms of TP) than checking if the jump kind is valid, since we'd have to check against multiple jump kinds each time. So maybe it's beneficial to allow |
Since In general, it makes sense to always null out I dislike |
The |
FYI: small TP diffs. |
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.
Mostly looks good...
|
||
bool JumpsTo(const BasicBlock* jumpDest) const | ||
{ | ||
return (bbJumpDest == jumpDest); |
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 seems odd that SetJumpKind
has a Compiler*
arg but SetJumpKindAndTarget
doesn't. I would expect the latter to be a logical superset.
If you added a Compiler*
arg you could then fix the switch version to allocate the BBswtDesc
automatically, perhaps.
@AndyAyersMS I initially intended for us to use Should I include that change here or in the follow-up PR? |
Follow up is fine, just trying to understand where things are headed. |
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.
Changes LGTM.
Followup to #93152. This refactor enforces new invariants on BasicBlock's bbJumpKind and bbJumpDest. In particular, whenever bbJumpKind is a kind that must have a jump target, bbJumpDest must be set, else bbJumpDest must be null. This means bbJumpKind and bbJumpDest must be simultaneously initialized/updated when creating/converting a jump block.
Next step for #93020. CC @dotnet/jit-contrib.