JIT: Treat store in JTRUE block as the ElseOperation in if-conversion#124738
JIT: Treat store in JTRUE block as the ElseOperation in if-conversion#124738BoyBaykiller wants to merge 5 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
|
@dotnet-policy-service agree |
208cc34 to
cba3e37
Compare
8997012 to
2ae217c
Compare
|
@a74nh @jakobbotsch PTAL |
| GenTreeLclVar* prevStore = tree->AsLclVar(); | ||
| if (prevStore->GetLclNum() == targetLclNum) | ||
| { | ||
| if (prevStore->Data()->IsInvariant()) |
There was a problem hiding this comment.
If we ever get a HIR version of IsInvariantInRange then we could use it here instead of IsInvariant. But for now this is the best I can do
…xecuted, so not inside Else block
There was a problem hiding this comment.
Pull request overview
This PR updates the CoreCLR JIT if-conversion phase to treat an unconditional prior store in the JTRUE block as the effective “else” value when forming a SELECT, so equivalent C# patterns yield equivalent IR and enable downstream optimizations (per #124713).
Changes:
- Replaces explicit
m_doElseConversionstate with aHasElseBlock()helper and usesm_elseOperation.block != nullptrto drive behavior. - Adds logic to locate a prior invariant
STORE_LCL_VARin the JTRUE block and use it as anElseOperationwhen there is no explicit else block. - Adjusts debug dumping/logging and CFG cleanup to handle both explicit else blocks and synthesized else operations.
| { | ||
| // There is no Else block, but we can still find an Else operation. Search for | ||
| // most recent STORE to the local in JTRUE block and see if it's legal to fwd sub | ||
| // it's definition into the SELECT and remove the STORE. |
There was a problem hiding this comment.
In this comment, "it's" should be the possessive "its" (no apostrophe).
| // it's definition into the SELECT and remove the STORE. | |
| // its definition into the SELECT and remove the STORE. |
| @@ -535,28 +590,36 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) | |||
|
|
|||
| // JTRUE block now contains SELECT. Change it's kind and make it flow | |||
There was a problem hiding this comment.
In this comment, "Change it's kind" should use the possessive "its" (no apostrophe).
| // JTRUE block now contains SELECT. Change it's kind and make it flow | |
| // JTRUE block now contains SELECT. Change its kind and make it flow |
| // There is no Else block, but we can still find an Else operation. Search for | ||
| // most recent STORE to the local in JTRUE block and see if it's legal to fwd sub | ||
| // it's definition into the SELECT and remove the STORE. | ||
|
|
||
| assert(m_mainOper == GT_STORE_LCL_VAR); | ||
|
|
||
| GenTreeLclVar* thenStore = m_thenOperation.node->AsLclVar(); | ||
| unsigned targetLclNum = thenStore->GetLclNum(); | ||
|
|
||
| bool usedInThenBlock = m_compiler->gtHasRef(thenStore->Data(), targetLclNum); | ||
| if (!usedInThenBlock) | ||
| { | ||
| Statement* last = m_startBlock->lastStmt(); | ||
| Statement* stmt = last; | ||
| do | ||
| { | ||
| GenTree* tree = stmt->GetRootNode(); | ||
| if (tree->OperIs(GT_STORE_LCL_VAR)) | ||
| { | ||
| GenTreeLclVar* prevStore = tree->AsLclVar(); | ||
| if (prevStore->GetLclNum() == targetLclNum) | ||
| { | ||
| if (prevStore->Data()->IsInvariant()) | ||
| { | ||
| m_elseOperation.block = m_startBlock; | ||
| m_elseOperation.stmt = stmt; | ||
| m_elseOperation.node = tree; | ||
| } | ||
|
|
||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (m_compiler->gtHasRef(tree, targetLclNum)) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| stmt = stmt->GetPrevStmt(); | ||
| } while (stmt != last); | ||
| } |
There was a problem hiding this comment.
The new path that synthesizes an ElseOperation from a previous STORE in the JTRUE block (when there is no explicit else block) changes if-conversion behavior but isn't covered by a JIT disasm-check regression test. Consider adding a small test case for the "bool x=false; if(cond){x=true;}" pattern to ensure it if-converts into a SELECT and enables the expected setcc/codegen improvement, so this doesn’t regress silently.
| Statement* last = m_startBlock->lastStmt(); | ||
| Statement* stmt = last; | ||
| do | ||
| { | ||
| GenTree* tree = stmt->GetRootNode(); | ||
| if (tree->OperIs(GT_STORE_LCL_VAR)) | ||
| { | ||
| GenTreeLclVar* prevStore = tree->AsLclVar(); | ||
| if (prevStore->GetLclNum() == targetLclNum) | ||
| { | ||
| if (prevStore->Data()->IsInvariant()) | ||
| { | ||
| m_elseOperation.block = m_startBlock; | ||
| m_elseOperation.stmt = stmt; | ||
| m_elseOperation.node = tree; | ||
| } | ||
|
|
||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (m_compiler->gtHasRef(tree, targetLclNum)) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| stmt = stmt->GetPrevStmt(); | ||
| } while (stmt != last); |
There was a problem hiding this comment.
An unbounded IR search like this is concerning. I would not be surprised if this can introduce JIT hangs in pathological cases.
This change to walk the IR seems like an additional change that is not needed for this PR. Can it be done in a separate PR and follow-up so that it can be evaluated in isolation?
There was a problem hiding this comment.
This sits at the heart of this PR. How should I find the most recent store into targetLclNum in m_startBlock without an IR walk?
There was a problem hiding this comment.
The pattern you are targeting looks like:
x = foo;
if (cond)
x = bar;
I would not expect to need to look backwards more than one statement from the JTRUE for this pattern.
There was a problem hiding this comment.
I would be ok with just bounding this walk to some constant, like try to find the statement within the previous 8 statements.
There was a problem hiding this comment.
Ok let me try.
There was a problem hiding this comment.
Given https://github.com/dotnet/runtime/pull/124738/changes#r3148881220 I would suggest not bothering. Instead I would propose to just validate that the conditional has no side effects (filters out both (2) and (3)) and then use gtTreeHasLocalRead to handle (1). If you want the search I suggest a follow-up PR.
| } | ||
| } | ||
|
|
||
| if (m_compiler->gtHasRef(tree, targetLclNum)) |
There was a problem hiding this comment.
gtHasRef is not a sufficient check here for a few reasons:
- It does not check for promoted cases, i.e. if
targetLclNumis a field and the parent local has a use, then the value will still depend on the value oftargetLclNum. SeefgCanMoveFirstStatementIntoPredfor how to handle this. - The local can have uses in EH successors that control gets transferred to by any intervening exception throw.
- If it is address exposed then not all uses are visible, and you really cannot reason about it in any meaningful fashion when intervening statements have global side effects.
There was a problem hiding this comment.
See fgCanMoveFirstStatementIntoPred for how to handle this.
Actually we have gtTreeHasLocalRead for a version of gtHasRef that handles promotion.
* update the 'is this lcl used?' check
|
The amount of diffs got halved. I am not sure that's more because we now bail if condition has side effects or that we only check the immediate predecessor. Like you said I can investigate and possibly improve this in a future PR. @jakobbotsch |
| bool unusedInThen = !m_compiler->gtTreeHasLocalRead(thenStore->Data(), targetLclNum); | ||
| bool unusedInCond = | ||
| ((m_cond->gtFlags & GTF_SIDE_EFFECT) == 0) && !m_compiler->gtTreeHasLocalRead(m_cond, targetLclNum); |
There was a problem hiding this comment.
If targetLclNum is address exposed then these checks are not sufficient. GTF_SIDE_EFFECT needs to be GTF_GLOB_EFFECT and unusedInThen needs to also check GTF_GLOB_REF.
Or you can just skip the opt if targetLclNum is address exposed. It is unlikely to be a common scenario in the diffs.
If-conversion phase now produces the same IR for these two cases:
The last unconditional store (
leftCloser = false) is substituted into the SELECT which enables further optimization. Specifically it fixes #124713.