Skip to content

Fix suboptimal codegen for short or patterns in is-expressions#82429

Open
stevenelliottjr wants to merge 2 commits intodotnet:mainfrom
stevenelliottjr:fix/pattern-match-codegen-regression
Open

Fix suboptimal codegen for short or patterns in is-expressions#82429
stevenelliottjr wants to merge 2 commits intodotnet:mainfrom
stevenelliottjr:fix/pattern-match-codegen-regression

Conversation

@stevenelliottjr
Copy link

Summary

Fixes #80052
Relates to #59615, #55334

For short or patterns (≤4 tests), the compiler now uses an inverted linear test sequence instead of the general DAG lowering. This eliminates unnecessary boolean temporaries and BoundSpillSequence wrapping that caused IL bloat.

For example, c is '/' or '\\' now produces:

!(c != '/' && c != '\\')

instead of the general DAG path which created a bool temp with true/false assignments and gotos.

Large or patterns (5+ tests) continue to use the general DAG lowering with switch dispatch (binary search trees), which is more efficient at scale.

IL size improvements

Pattern Before After
o is int or long (Debug) 26 bytes 21 bytes
o is int or long (Release) 24 bytes 20 bytes
(o is int or long) ? "True" : "False" (Debug) 40 bytes 29 bytes
x > 0 && c is '/' or '\\' (Release) 19 bytes (no bool temp)

Why this approach vs previous attempts

This problem was previously addressed in PR #68694 (reverted in #69582) and PR #72273 (reverted in #72827 due to compiler crash #72753). Both previous attempts modified the general DAG lowering to avoid the result temp, which was fragile because it interacted with exception handler scopes (await using, try/finally), causing NullReferenceException in ILBuilder.BlockedBranchDestinationSlow.

This PR takes a fundamentally different approach:

  • Does not modify the general DAG lowering at all
  • Routes short or patterns to the existing, well-tested linear rewriter with swapped labels
  • Adds a maxTests parameter to canProduceLinearSequence to limit the optimization to short sequences
  • The linear rewriter doesn't use SpillSequence and doesn't interact with exception handler scopes, avoiding the crashes that killed previous attempts

Changes

  • LocalRewriter_IsPatternOperator.cs: Added inverted linear sequence path for short or patterns; added maxTests parameter to canProduceLinearSequence
  • PatternTests.cs: Updated IL expectations for IsPatternDisjunct_01 and _02; added new test IsPatternDisjunct_OrPatternInAndExpression for the exact scenario from Suboptimal codegen for is pattern compared to .NET 6/7 #80052

Test plan

…net#80052)

For short `or` patterns (up to 4 tests), use an inverted linear test
sequence instead of the general DAG lowering. The general path introduces
unnecessary bool temporaries and a SpillSequence, producing larger and
slower IL. The inverted linear path produces clean boolean expressions
(e.g., `!(x != '/' && x != '\\')`) that the IL emitter handles efficiently.

For large `or` patterns, the general DAG lowering with switch dispatch
(binary search trees) is still preferred.
@stevenelliottjr stevenelliottjr requested a review from a team as a code owner February 17, 2026 12:40
@dotnet-policy-service dotnet-policy-service bot added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Feb 17, 2026
@stevenelliottjr
Copy link
Author

@dotnet-policy-service agree

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Feb 17, 2026

@stevenelliottjr It looks like there are legitimate test failures. #Closed

@stevenelliottjr
Copy link
Author

Apologies for the noise, @AlekseyTs. I'll investigate the test failures and get them resolved.

The inverted linear sequence optimization for short `or` patterns
followed "failure" branches, skipping BoundWhenDecisionDagNodes that
contain variable bindings. This left captured variables uninitialized,
causing NullReferenceException for patterns like `(B or C) and var x`.

Add dagContainsBindings guard to fall back to general DAG lowering when
the pattern contains variable captures. Update IL baselines for Span
pattern tests to reflect the new (correct, smaller) codegen.
@stevenelliottjr stevenelliottjr force-pushed the fix/pattern-match-codegen-regression branch from f37fda5 to 8e13d09 Compare February 17, 2026 20:55
@stevenelliottjr
Copy link
Author

@AlekseyTs I've pushed a fix. The issue was that the new inverted linear sequence optimization was skipping BoundWhenDecisionDagNodes with variable bindings — patterns like (B or C) and var x would leave captured variables uninitialized. I added a dagContainsBindings guard so those patterns fall back to the general DAG lowering. Also updated the Span pattern IL baselines. All 1350 PatternMatchingTests pass with zero regressions.

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Feb 18, 2026

Large or patterns (5+ tests) continue to use the general DAG lowering with switch dispatch (binary search trees), which is more efficient at scale.

Is this just an assumption, or this statement is supported by some real numbers? #Closed

// linear tests with a single "golden" path to the true label and all other paths leading
// to the false label? This occurs with an is-pattern expression that uses no "or" or "not"
// pattern forms.
// When maxTests is specified, the sequence is limited to at most that many test nodes.
Copy link
Contributor

Choose a reason for hiding this comment

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

When maxTests is specified, the sequence is limited to at most that many test nodes.

I think "When maxTests is specified" part is somewhat confusing. Consider adjusting to: "maxTests limits the amount BoundTestDecisionDagNode nodes in the sequence."

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Feb 18, 2026

Large or patterns (5+ tests) continue to use the general DAG lowering with switch dispatch (binary search trees), which is more efficient at scale.

Is this just an assumption, or this statement is supported by some real numbers?

I guess this is just a rephrase of an existing comment in implementation #Closed

@AlekseyTs
Copy link
Contributor

            isPatternRewriter.Free();

It looks like this code is a copy of the previous block body. Consider extracting and reusing a common helper.


Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IsPatternOperator.cs:57 in 8e13d09. [](commit_id = 8e13d09, deletion_comment = False)

{
foreach (var node in decisionDag.TopologicallySortedNodes)
{
if (node is BoundWhenDecisionDagNode { Bindings.Length: > 0 })
Copy link
Contributor

Choose a reason for hiding this comment

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

Length: > 0

IsEmpty: false?

compilation.VerifyDiagnostics();
var expectedOutput = @"TrueTrueFalseFalse";
var compVerifier = CompileAndVerify(compilation, expectedOutput: expectedOutput);
// The `is '/' or '\\'` pattern should produce comparable IL to `c == '/' || c == '\\'`
Copy link
Contributor

Choose a reason for hiding this comment

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

// The is '/' or '\\' pattern should produce comparable IL to c == '/' || c == '\\'

It feels like we should assert that M2 results in the same IL

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 2)

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

Labels

Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Suboptimal codegen for is pattern compared to .NET 6/7

2 participants

Comments