Skip to content
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: Add explicit successor for BBJ_COND false branch #95773

Merged
merged 34 commits into from
Dec 11, 2023

Conversation

amanasifkhalid
Copy link
Member

Part of #93020. Currently, if a BBJ_COND block's bbJumpDest branch is not taken, control falls through into the next block. Because we plan to remove this fall-through requirement, BBJ_COND blocks need a pointer to their "not-taken" branch target, as the target may no longer be the next block. This new pointer, bbNormalJumpDest, may also be used in the future to represent the finally continuation of a BBJ_CALLFINALLY/BBJ_ALWAYS pair, once this pattern is consolidated into one block with no implicit fall-through via #95355.

For now, bbNormalJumpDest models current behavior by always pointing to the next block. Follow-up work will introduce behavioral changes by allowing bbNormalJumpDest to diverge from bbNext.

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Dec 7, 2023
@ghost ghost assigned amanasifkhalid Dec 7, 2023
@ghost
Copy link

ghost commented Dec 7, 2023

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

Part of #93020. Currently, if a BBJ_COND block's bbJumpDest branch is not taken, control falls through into the next block. Because we plan to remove this fall-through requirement, BBJ_COND blocks need a pointer to their "not-taken" branch target, as the target may no longer be the next block. This new pointer, bbNormalJumpDest, may also be used in the future to represent the finally continuation of a BBJ_CALLFINALLY/BBJ_ALWAYS pair, once this pattern is consolidated into one block with no implicit fall-through via #95355.

For now, bbNormalJumpDest models current behavior by always pointing to the next block. Follow-up work will introduce behavioral changes by allowing bbNormalJumpDest to diverge from bbNext.

Author: amanasifkhalid
Assignees: amanasifkhalid
Labels:

area-CodeGen-coreclr

Milestone: -

@amanasifkhalid amanasifkhalid marked this pull request as ready for review December 8, 2023 01:04
@@ -573,11 +576,18 @@ struct BasicBlock : private LIR::Range

void SetNext(BasicBlock* next)
{
// TODO: remove
Copy link
Member Author

Choose a reason for hiding this comment

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

I meant to delete this; will get rid of it in the next revision.

@@ -2957,6 +2957,13 @@ void Compiler::fgDebugCheckBBlist(bool checkBBNum /* = false */, bool checkBBRef

maxBBNum = max(maxBBNum, block->bbNum);

// BBJ_COND's normal (false) jump target is expected to be the next block
// TODO: Allow bbNormalJumpDest to diverge from bbNext
Copy link
Contributor

Choose a reason for hiding this comment

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

A little tip for writing future TODOs: in ongoing work, it is handy to add a unique postfix to a TODO, e. g. TODO-NoFallThrough, s. t. they can be text-searched for easily afterwards.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for the tip; I realized this might be a better approach about halfway through... I'll update the TODOs I introduced to make them easier to Ctrl+F when I remove them in the next PR.

@amanasifkhalid
Copy link
Member Author

CC @dotnet/jit-contrib, @AndyAyersMS PTAL -- sorry for the wait on this. No diffs except for TP impact.

@BruceForstall
Copy link
Member

What is the terminology we want to use for the two BBJ_COND branches? With this PR we now have "bbNormalJumpDest" indicating the "false" path and "bbJumpDest" indicating the "true" path. Neither of these words seem satisfactory to me, especially "normal". It seems like we should have "bbFalseJumpDest" and "bbTrueJumpDest" for BBJ_COND. For cases with only a single destination (e.g., BBJ_ALWAYS), just "bbJumpDest" seems fine.

We over-emphasize the word "jump"; maybe we can remove it from all those places, instead having "bbFalseDest", "bbTrueDest", "bbDest", "bbSwtDest", "bbEhfDest", "bbKind", "BBKinds", etc.

And maybe "Dest" should instead be "Target": "bbFalseTarget", "bbTrueTarget", "bbTarget", "bbSwtTarget", "bbEhfTarget". I think this is my preferred option.

@dotnet/jit-contrib Comments?

@SingleAccretion
Copy link
Contributor

FWIW, here's the origin of the "normal" terminology: #93772 (comment).

I agree it would be best to use "True/False", the problem was/is that we use GetJumpDest for the "true" path currently.

@amanasifkhalid
Copy link
Member Author

I agree this would be a good time to rename these members.

It seems like we should have "bbFalseJumpDest" and "bbTrueJumpDest" for BBJ_COND.

For bbTrueTarget, are you suggesting we introduce a new BasicBlock* member to the union of jump targets that essentially functions as an alias for bbJumpDest, and then rename bbNormalJumpDest to bbFalseTarget? Or would you prefer a new jump kind altogether for BBJ_COND, like we do for BBJ_SWITCH? This new type would be a pointer to a struct of two BasicBlock* members: the true and the false targets. This would also come with new helper methods specific to BBJ_COND (like SetJumpKindAndTarget, etc.) for manipulating this new jump kind.

I concur with your other naming suggestions, @BruceForstall.

@BruceForstall
Copy link
Member

For bbTrueTarget, are you suggesting we introduce a new BasicBlock* member to the union of jump targets that essentially functions as an alias for bbJumpDest, and then rename bbNormalJumpDest to bbFalseTarget?

Yes, this is what I was thinking.

Yes, there would be new helpers that would assert on BBJ_COND and return/set the new union member.

This probably means various "switch" cases where BBJ_COND behavior re-uses the same "branch" behavior and bbJumpDest access as other kinds would have to be split out, to access the new member. Maybe that's ok anyway, and clearer. (And the native compilers hopefully will "re-merge" if possible in the generated code.)

@AndyAyersMS
Copy link
Member

I agree true/false makes more sense, as abstractly before block layout all control flow transfers are jumps.

Ultimately, I would like to see the blocks refer to FlowEdges instead of directly to blocks, so for BBJ_COND we'd have say TrueEdge and FalseEdge, and we'd enhance FlowEdge to refer to the target block (currently implicit); but we could also have combo methods that then access the edge and then its target. But if/when we get to this point we can still have BasicBlock* valued TrueTarget and FalseTarget methods on a block, they'll just do a bit more work.

@BruceForstall
Copy link
Member

Ultimately, ...

I presume you're stating this as a long-term goal, and not something we should consider for this, or a short-term PR? Thus, we can evolve in that direction but still take name changes and block pointer implementation as proposed (if agreed to)?

@amanasifkhalid
Copy link
Member Author

I've renamed the BasicBlock members @BruceForstall mentioned above, and added new helper methods for setting/accessing/checking BBJ_COND's true and false successors. I didn't add a BBJ_COND-specific variant of SetKindAndTarget in this change, as that might be best reserved for follow-up work once the false successor can diverge from bbNext, and we need a way to explicitly set both the true and false targets -- would we like a separate definition of SetKindAndTarget for initializing bbTrueTarget?

I apologize for the size of this change; at the very least, it does not produce any asmdiffs locally.

@jakobbotsch
Copy link
Member

would we like a separate definition of SetKindAndTarget for initializing bbTrueTarget?

I think splitting these overloads to separate methods would make sense -- e.g. SetSwitch, SetCond and so on.

return (bbFalseTarget == target);
}

void SetCondKindAndTarget(BasicBlock* target)
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't exactly roll off the tongue -- why not SetCond? Eventually we would hopefully end up with just SetCond(BasicBlock* falseTarget, BasicBlock* trueTarget), SetSwitch(BBswtDesc* swtTarget) and so 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.

I can shorten the names of all the SetKind... variants. Would you be ok with SetKindAndTarget being left as-is? I'm not sure how we can intuitively shorten that one while still conveying it's setting the jump kind and target.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think SetKindAndTarget makes sense -- it's sort of a low-level operation, while these other ones are more high-level.

Copy link
Member

@AndyAyersMS AndyAyersMS left a comment

Choose a reason for hiding this comment

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

A few small suggestions. Feel free to defer those to a subsequent PR.

}

BBswtDesc* GetJumpSwt() const
BBswtDesc* GetSwitchTarget() const
Copy link
Member

Choose a reason for hiding this comment

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

Nit: switches have multiple targets, so maybe?

Suggested change
BBswtDesc* GetSwitchTarget() const
BBswtDesc* GetSwitchTargets() const

}

BBehfDesc* GetJumpEhf() const
BBehfDesc* GetEhfTarget() const
Copy link
Member

Choose a reason for hiding this comment

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

Similar nit

Suggested change
BBehfDesc* GetEhfTarget() const
BBehfDesc* GetEhfTargets() const

assert(HasInitializedJumpDest());
return (bbJumpDest == jumpDest);
// BBJ_COND should use TrueTargetIs
assert(!KindIs(BBJ_COND));
Copy link
Member

Choose a reason for hiding this comment

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

Seems like you should also assert for BBJ_SWITCH and BBJ_EHFINALLYRET in these (here and elsewhere).

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we're already implicitly asserting for BBJ_SWITCH and BBJ_EHFINALLYRET by asserting HasInitializedTarget, as that asserts HasTarget, which checks KindIs(BBJ_ALWAYS, BBJ_CALLFINALLY, BBJ_COND, BBJ_EHCATCHRET, BBJ_EHFILTERRET, BBJ_LEAVE). But that's a lot of indirection, so I can explicitly assert for BBJ_SWITCH and BBJ_EHFINALLYRET, too.

}

void SetJumpEhf(BBehfDesc* jumpEhf)
void SetEhfTarget(BBehfDesc* ehfTarget)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void SetEhfTarget(BBehfDesc* ehfTarget)
void SetEhfTargets(BBehfDesc* ehfTargets)

unsigned bbTargetOffs; // PC offset (temporary only)
BasicBlock* bbTarget; // basic block
BasicBlock* bbTrueTarget; // BBJ_COND jump target when its condition is true (alias for bbTarget)
BBswtDesc* bbSwtTarget; // switch descriptor
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
BBswtDesc* bbSwtTarget; // switch descriptor
BBswtDesc* bbSwtTargets; // switch descriptor

BasicBlock* bbTarget; // basic block
BasicBlock* bbTrueTarget; // BBJ_COND jump target when its condition is true (alias for bbTarget)
BBswtDesc* bbSwtTarget; // switch descriptor
BBehfDesc* bbEhfTarget; // BBJ_EHFINALLYRET descriptor
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
BBehfDesc* bbEhfTarget; // BBJ_EHFINALLYRET descriptor
BBehfDesc* bbEhfTargets; // BBJ_EHFINALLYRET descriptor

BasicBlock* GetTarget() const
{
// BBJ_COND should use GetTrueTarget
assert(!KindIs(BBJ_COND));
Copy link
Member

Choose a reason for hiding this comment

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

Seems like you should also assert for BBJ_SWITCH and BBJ_EHFINALLYRET in these (here and elsewhere).

void SetTarget(BasicBlock* target)
{
// BBJ_COND should use SetTrueTarget
assert(!KindIs(BBJ_COND));
Copy link
Member

Choose a reason for hiding this comment

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

Seems like you should also assert for BBJ_SWITCH and BBJ_EHFINALLYRET in these (here and elsewhere).

// `block` (inclusive). Thus, we need to ensure there is a label on the lexical fall-through
// block, even if one is not otherwise needed, to be able to calculate the size of this
// loop (loop size is calculated by walking the instruction groups; see emitter::getLoopSize()).

if (block->GetJumpDest()->isLoopAlign())
if (block->GetTarget()->isLoopAlign())
Copy link
Member

Choose a reason for hiding this comment

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

This kind of code duplication is a bit unfortunate. Obviously early on there is a distinction for BBJ_COND but later on there isn't...

One half-baked idea is to have some variant of BBJ_COND that only appears late, once we allow fall throughs, and can't appear earlier. But let's just leave this as is for now.

@amanasifkhalid
Copy link
Member Author

@AndyAyersMS thank you for the review; I've applied your feedback. The new asserts you suggested triggered in Compiler::fgSplitBlockAtEnd, as we were previously calling GetTarget() on a BBJ_EHFINALLYRET block, and then creating a new BBJ_EHFINALLYRET with BasicBlock::New(Compiler* compiler, BBKinds kind, BasicBlock* target). I've refactored fgSplitBlockAtEnd by creating a new variant of BasicBlock::New specifically for BBJ_EHFINALLYRET (kind of like how we have a variant for BBJ_SWITCH), and moving the logic for creating new blocks in fgSplitBlockAtEnd into a switch statement. It's slightly more code duplication, but almost all of the logic after the block creation is generalized across block types in that method.

@amanasifkhalid
Copy link
Member Author

OSX arm64 NativeAOT failure looks unrelated.

@amanasifkhalid amanasifkhalid merged commit 6c7e6e2 into dotnet:main Dec 11, 2023
124 of 129 checks passed
@jakobbotsch
Copy link
Member

I hit asserts with JitDump enabled after this change in some simple code examples. For example

[MethodImpl(MethodImplOptions.NoInlining)]
static int abs(int num) => num < 0 ? -num : num;

hits

Assert failure(PID 21588 [0x00005454], Thread: 51020 [0xc74c]): Assertion failed 'KindIs(BBJ_COND)' in 'Program:abs(int):int' during 'If conversion' (IL size 9; hash 0x993542c0; FullOpts)

    File: C:\dev\dotnet\runtime4\src\coreclr\jit\block.h Line: 661
    Image: D:\dev\core_roots\6c7e6e2e50550737260e40f0b472158bc15a6f82\corerun.exe

when DOTNET_JitDump=abs. Can you take a look? (Also might be a good idea to run SPMI through with JitDump=*).

@amanasifkhalid
Copy link
Member Author

@jakobbotsch Thank you for letting me know, taking a look now...

amanasifkhalid added a commit that referenced this pull request Dec 12, 2023
…#95934)

In #95773, I incorrectly assumed m_startBlock would always be a BBJ_COND in OptIfConversionDsc::IfConvertDump, but it can be converted to a BBJ_ALWAYS before being dumped, thus hitting an assert when m_startBlock->GetTrueTarget() is called. This is fixed by calling the correct target accessor method depending on the type of m_startBlock.

(Note that IfConvertDump is only called if dumps are enabled, hence why this assert wasn't initially hit in CI.)
@github-actions github-actions bot locked and limited conversation to collaborators Jan 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants