Skip to content

JIT: Treat store in JTRUE block as the ElseOperation in if-conversion#124738

Open
BoyBaykiller wants to merge 5 commits intodotnet:mainfrom
BoyBaykiller:if-conv-unconditional-store-to-else-124713
Open

JIT: Treat store in JTRUE block as the ElseOperation in if-conversion#124738
BoyBaykiller wants to merge 5 commits intodotnet:mainfrom
BoyBaykiller:if-conv-unconditional-store-to-else-124713

Conversation

@BoyBaykiller
Copy link
Copy Markdown
Contributor

@BoyBaykiller BoyBaykiller commented Feb 23, 2026

If-conversion phase now produces the same IR for these two cases:

bool First(int tMinLeft, int tMinRight)
{
    bool leftCloser = false;
    if (tMinLeft < tMinRight)
    {
        leftCloser = true;
    }
    return leftCloser;
}

bool Second(int tMinLeft, int tMinRight)
{
    bool leftCloser;
    if (tMinLeft < tMinRight)
    {
        leftCloser = true;
    }
    else
    {
        leftCloser = false;
    }
    return leftCloser;
}

The last unconditional store (leftCloser = false) is substituted into the SELECT which enables further optimization. Specifically it fixes #124713.

@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Feb 23, 2026
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Feb 23, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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

@BoyBaykiller
Copy link
Copy Markdown
Contributor Author

@dotnet-policy-service agree

@BoyBaykiller BoyBaykiller marked this pull request as ready for review February 24, 2026 19:40
@BoyBaykiller BoyBaykiller marked this pull request as draft February 24, 2026 20:01
@BoyBaykiller BoyBaykiller changed the title JIT: Treat unconditional store as the else case in if-conversion JIT: Treat store in JTRUE block as the ElseOperation in if-conversion Mar 10, 2026
@BoyBaykiller BoyBaykiller force-pushed the if-conv-unconditional-store-to-else-124713 branch from 208cc34 to cba3e37 Compare March 19, 2026 00:48
@BoyBaykiller BoyBaykiller force-pushed the if-conv-unconditional-store-to-else-124713 branch from 8997012 to 2ae217c Compare April 15, 2026 18:29
@BoyBaykiller BoyBaykiller reopened this Apr 16, 2026
@BoyBaykiller BoyBaykiller marked this pull request as ready for review April 16, 2026 04:07
@BoyBaykiller
Copy link
Copy Markdown
Contributor Author

@a74nh @jakobbotsch PTAL

Comment thread src/coreclr/jit/ifconversion.cpp Outdated
GenTreeLclVar* prevStore = tree->AsLclVar();
if (prevStore->GetLclNum() == targetLclNum)
{
if (prevStore->Data()->IsInvariant())
Copy link
Copy Markdown
Contributor Author

@BoyBaykiller BoyBaykiller Apr 16, 2026

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_doElseConversion state with a HasElseBlock() helper and uses m_elseOperation.block != nullptr to drive behavior.
  • Adds logic to locate a prior invariant STORE_LCL_VAR in the JTRUE block and use it as an ElseOperation when there is no explicit else block.
  • Adjusts debug dumping/logging and CFG cleanup to handle both explicit else blocks and synthesized else operations.

Comment thread src/coreclr/jit/ifconversion.cpp Outdated
{
// 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.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

In this comment, "it's" should be the possessive "its" (no apostrophe).

Suggested change
// it's definition into the SELECT and remove the STORE.
// its definition into the SELECT and remove the STORE.

Copilot uses AI. Check for mistakes.
Comment thread src/coreclr/jit/ifconversion.cpp Outdated
@@ -535,28 +590,36 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget)

// JTRUE block now contains SELECT. Change it's kind and make it flow
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

In this comment, "Change it's kind" should use the possessive "its" (no apostrophe).

Suggested change
// 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

Copilot uses AI. Check for mistakes.
Comment thread src/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +106 to +146
// 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);
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +118 to +145
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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member

@jakobbotsch jakobbotsch Apr 27, 2026

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would be ok with just bounding this walk to some constant, like try to find the statement within the previous 8 statements.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok let me try.

Copy link
Copy Markdown
Member

@jakobbotsch jakobbotsch Apr 27, 2026

Choose a reason for hiding this comment

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

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.

Comment thread src/coreclr/jit/ifconversion.cpp Outdated
}
}

if (m_compiler->gtHasRef(tree, targetLclNum))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

gtHasRef is not a sufficient check here for a few reasons:

  • It does not check for promoted cases, i.e. if targetLclNum is a field and the parent local has a use, then the value will still depend on the value of targetLclNum. See fgCanMoveFirstStatementIntoPred for 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
@BoyBaykiller
Copy link
Copy Markdown
Contributor Author

BoyBaykiller commented Apr 27, 2026

The amount of diffs got halved.
Old arm64 -25,036
New arm64 -16,324

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

Comment on lines +116 to +118
bool unusedInThen = !m_compiler->gtTreeHasLocalRead(thenStore->Data(), targetLclNum);
bool unusedInCond =
((m_cond->gtFlags & GTF_SIDE_EFFECT) == 0) && !m_compiler->gtTreeHasLocalRead(m_cond, targetLclNum);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JIT: Turn conditional move arround bool into setl to save registers

3 participants