Skip to content

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

Open
BoyBaykiller wants to merge 11 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 11 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
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 thread src/coreclr/jit/ifconversion.cpp Outdated
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.

* add budget to stmt search
* ignore address exposed lcls
@BoyBaykiller
Copy link
Copy Markdown
Contributor Author

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

I tested locally and the majority comes from the GTF_SIDE_EFFECT check, however in the real world I expect the stmtSearchBudget to have a larger impact. I now ignore address exposed variables as you said and I wonder if I still need to check GTF_PERSISTENT_SIDE_EFFECTS | GTF_EXCEPT ( GTF_SIDE_EFFECT)


Regarding GTF_PERSISTENT_SIDE_EFFECTS: I use gtTreeHasLocalStore instead.

Regarding GTF_EXCEPT: Removing this brings by far the biggest diffs. You said "The local can have uses in EH successors that control gets transferred to by any intervening exception throw". I am trying to come up with a reproducible for this but I am having trouble. Conceptually I think you mean something like this:

void M0(int var1, ref bool throwingCond)
{
    var1 = 1;
    try
    {
        if (throwingCond) // <- might throw
        {
            var1 = 0;
        }
    }
    catch (Exception)
    {
        // used in EH
        var1 = 5;
    }

    Console.WriteLine(var1);
}

But in practice the var1 = 1; is not part of the same block as the JTRUE. So I am hoping we don't need GTF_EXCEPT either

Comment thread src/coreclr/jit/ifconversion.cpp Outdated
return false;
}

int stmtSearchBudget = 2;
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.

Note 2 includes the jtrue stmt itself. So we actually only look back 1 stmt

@jakobbotsch
Copy link
Copy Markdown
Member

Regarding GTF_EXCEPT: Removing this brings by far the biggest diffs. You said "The local can have uses in EH successors that control gets transferred to by any intervening exception throw". I am trying to come up with a reproducible for this but I am having trouble. Conceptually I think you mean something like this:

void M0(int var1, ref bool throwingCond)
{
    var1 = 1;
    try
    {
        if (throwingCond) // <- might throw
        {
            var1 = 0;
        }
    }
    catch (Exception)
    {
        // used in EH
        var1 = 5;
    }

    Console.WriteLine(var1);
}

But in practice the var1 = 1; is not part of the same block as the JTRUE. So I am hoping we don't need GTF_EXCEPT either

Close, I mean something like:

using System;
using System.Runtime.CompilerServices;

public class Program
{
    public static void Main()
    {
        Foo(null);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void Foo(C c)
    {
        int x = 0;
        try
        {
            x = 1;
            if (c.Field == 2)
                x = 2;
        }
        catch
        {
            Console.WriteLine(x);
        }
    }

    class C
    {
        public int Field;
    } 
}

Main: Prints 1
This PR: Prints 0

If this case is important for diffs you can use BasicBlock::HasPotentialEHSuccs. If false, control cannot resume in the same function after an exception is thrown. To be even more precise you can use LclVarDsc::lvLiveInOutOfHndlr, but I doubt the latter would bring many (if any) improvements, so I would not bother with the complexity.

@BoyBaykiller
Copy link
Copy Markdown
Contributor Author

Thank you for the repro! That really helps my understanding.

If this case is important for diffs you can use BasicBlock::HasPotentialEHSuccs. If false, control cannot resume in the same function after an exception is thrown. To be even more precise you can use LclVarDsc::lvLiveInOutOfHndlr, but I doubt the latter would bring many (if any) improvements, so I would not bother with the complexity.

I didn't know these existed, that's helpful. lvLiveInOutOfHndlr gives me exactly what I needed and is free. I think having the precise check actually makes the code more self-documenting in a way. And it fits right in with the existing IsAddressExposed check, so I'd prefer that over BasicBlock::HasPotentialEHSuccs.

I guess we could be even more precise by checking for GTF_EXCEPT in combination with lvLiveInOutOfHndlr : ) But that would certainly add useless complexity so I wont do that.

Comment thread src/coreclr/jit/ifconversion.cpp Outdated
// Cannot easily reason about all uses if it is address exposed or
// used in EH where control could get transferred to at "any time"
LclVarDsc* lclVarDsc = m_compiler->lvaGetDesc(targetLclNum);
if (lclVarDsc->IsAddressExposed() || lclVarDsc->lvLiveInOutOfHndlr)
Copy link
Copy Markdown
Member

@jakobbotsch jakobbotsch Apr 28, 2026

Choose a reason for hiding this comment

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

lvLiveInOutOfHndlr is accurate only for tracked locals. It cannot be used without a check for lvTracked and fallback to more conservative behavior in the untracked case.

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.

Oh ok lol. I am learning as I am going. Then I'll use BasicBlock::HasPotentialEHSuccs.

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 suggest HasPotentialEHSuccs combined with the exception check, yeah.

@BoyBaykiller
Copy link
Copy Markdown
Contributor Author

I've made the EH check more precise to avoid spurious diffs as you said. Now this still gets if-converted:

void Foo(bool cond, C c)
{
    int x = 2;
    try
    {
        _ = c.Field; // GTF_EXCEPT before STORE, don't bail
        x = 0;
        if (cond)
            x = 1;
    }
    catch
    {
        Console.WriteLine(x);
    }
}

@BoyBaykiller
Copy link
Copy Markdown
Contributor Author

The diffs look better again. Upping the stmtSearchBudget a little should get us back to where we were before, but if I understood correctly you wish to have that be a separate PR

@jakobbotsch
Copy link
Copy Markdown
Member

The diffs look better again. Upping the stmtSearchBudget a little should get us back to where we were before, but if I understood correctly you wish to have that be a separate PR

I'm ok with doing it here given that we have all the machinery anyway to deal with the JTRUE interference.

Comment on lines +301 to +304
if (((tree->gtFlags & GTF_EXCEPT) != 0) && hasEhSuccs)
{
break;
}
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 think this should check for GTF_ORDER_SIDEEFF as well.

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.

Why is that check needed? It makes the diffs bad again.

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