Skip to content

Conversation

ashay
Copy link

@ashay ashay commented Aug 13, 2025

Prior to this patch, when emitting code for x64, if an instruction
updated the flags register and if the following instruction was an
equals or not-equals comparison with zero, the runtime elided the test
instruction for materializing the comparison.

Although correct, this is limiting, since it excludes the predicates
SGT, SGE, SLT, and SLE. This patch extends the optimization to allow the
omission of the test instruction for all signed integer comparisons.
We skip unsigned integer comparisons since unsigned comparisons with
zero can be evaluated earlier in the compilation process.

Fix #117866

Prior to this patch, when emitting code for x64, if an instruction
updated the flags register and if the following instruction was an
equals or not-equals comparison with zero, the runtime elided the `test`
instruction for materializing the comparison.

Although correct, this is limiting, since it excludes the predicates
SGT, SGE, SLT, and SLE. This patch extends the optimization to allow the
omission of the `test` instruction for all signed integer comparisons.
We skip unsigned integer comparisons since unsigned comparisons with
zero can be evaluated earlier in the compilation process.

Fix dotnet#117866
@ashay
Copy link
Author

ashay commented Aug 13, 2025

I'd like to add a unit test but I am not sure what the right place is for adding code-gen tests. So far, I have the following patch, but if there's a better location / test, please let me know. Thanks!

diff --git a/src/tests/JIT/opt/Compares/compares.cs b/src/tests/JIT/opt/Compares/compares.cs
index f08c65bf4dc..f2231ab7840 100644
--- a/src/tests/JIT/opt/Compares/compares.cs
+++ b/src/tests/JIT/opt/Compares/compares.cs
@@ -345,6 +345,23 @@ public static void Le_else_double_int_consume(double f1, double f2, int a1, int
         consume<double>(a1, a2);
     }
 
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    [Theory]
+    [InlineData(10)]
+    public static void Check_elide_test_insn_on_x64(int x)
+    {
+        // X64-NOT:  test {{.*}}, {{.*}}
+        while (true)
+        {
+            x += 1;
+            if (x > 0)
+            {
+                break;
+            }
+        }
+        consume<int>(x);
+    }
+
     /* If/Else conditions that return. */
 
     [MethodImpl(MethodImplOptions.NoInlining)]

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 13, 2025
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Aug 13, 2025
@huoyaoyuan huoyaoyuan added area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Aug 13, 2025
Copy link
Contributor

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

Comment on lines 1467 to 1469
if ((cond.GetCode() == GenCondition::NE) || (cond.GetCode() == GenCondition::EQ) ||
(cond.GetCode() == GenCondition::SLE) || (cond.GetCode() == GenCondition::SLT) ||
(cond.GetCode() == GenCondition::SGE) || (cond.GetCode() == GenCondition::SGT))
Copy link
Member

Choose a reason for hiding this comment

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

The code below requires only the zero flag to be written, but that's not sufficient for these other conditions. I expect that's the cause of the test failures.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the tip! I've added the checks for other flag bits and separated them by the condition code so that flag checks are specific to the requested condition code.

Add new checks for specific flag bits based on the requested condition
predicate
@ashay
Copy link
Author

ashay commented Aug 14, 2025

@dotnet-policy-service agree

@ashay
Copy link
Author

ashay commented Aug 15, 2025

I see almost all x64 tests fail, so it doesn't look like this change is safe. Perhaps we should check whether the test instruction's consumer looks up just OF, SF, ZF, and PF or whether it references more flags than those four. I'm peering over the x64 ISA using a theorem prover to see if I can find a counter example, but since that will take a while, I'm closing this PR for now.

@ashay ashay closed this Aug 15, 2025
@ashay ashay deleted the issue-117866 branch August 15, 2025 03:32
@xtqqczze
Copy link
Contributor

xtqqczze commented Aug 15, 2025

Perhaps we should check whether the test instruction's consumer looks up just OF, SF, ZF, and PF or whether it references more flags than those four.

adc, sbb, and various jump instructions all reference CF, which test sets to zero.

@ashay
Copy link
Author

ashay commented Aug 15, 2025

adc, sbb, and various jump instructions all reference CF, which test sets to zero.

Indeed, you're right, but I was wondering in the context of the SGE, SLE, SGT, and SLT comparisons. It seems like CF shouldn't matter for those comparisons.

Copy link
Contributor

@xtqqczze xtqqczze left a comment

Choose a reason for hiding this comment

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

test instruction resets OF


if ((cond.GetCode() == GenCondition::SLT) || (cond.GetCode() == GenCondition::SGE))
{
if (DoesWriteSignFlag(lastIns) && DoesWriteOverflowFlag(lastIns) && IsFlagsAlwaysModified(id))
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (DoesWriteSignFlag(lastIns) && DoesWriteOverflowFlag(lastIns) && IsFlagsAlwaysModified(id))
if (DoesResetOverflowFlag(lastIns) && DoesWriteSignFlag(lastIns) && IsFlagsAlwaysModified(id))


if ((cond.GetCode() == GenCondition::SGT) || (cond.GetCode() == GenCondition::SLE))
{
if (DoesWriteZeroFlag(lastIns) && DoesWriteSignFlag(lastIns) && DoesWriteOverflowFlag(lastIns) &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (DoesWriteZeroFlag(lastIns) && DoesWriteSignFlag(lastIns) && DoesWriteOverflowFlag(lastIns) &&
if (DoesResetOverflowFlag(lastIns) && DoesWriteZeroFlag(lastIns) && DoesWriteSignFlag(lastIns) &&

@ashay ashay restored the issue-117866 branch August 16, 2025 03:42
@ashay
Copy link
Author

ashay commented Aug 16, 2025

Thanks for the suggested fix! I unfortunately botched the branch by force-pushing after rebasing with main, so I couldn't re-open this PR. Here's the new PR instead: #118806.

I agree that we should check whether lastIns resets OF and CF before removing the test instruction to match the semantics, but sadly, it doesn't resolve the original problem reported in #117866, since the add instruction doesn't always reset OF and CF.

Perhaps I misunderstand, but I wonder if there is a way to check the consumer of the flags to determine if we can ignore the updates to OF and CF. I can see how this would make the analysis fairly complicated since use-def chains for flags aren't explicitly encoded in the data flow, but if there's some existing logic that would help, please let me know. Thanks!

@github-actions github-actions bot locked and limited conversation to collaborators Sep 15, 2025
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 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.

Redundant TEST instruction after ADD
4 participants