Skip to content

Optimize constant-range patterns whose Boolean result is consumed by a switch expression #135630

Open

Description

@svick

Description

Constant-range patterns can produce less efficient native code than an equivalent subtraction-based helper when their Boolean result is consumed by a switch expression.

This is related to:

The remaining cases in the original Reddit parser were reproduced with a JIT containing that fix:

public static string? Validate(string iban, string pattern)
{
    for (int i = 0; i < iban.Length; i++)
    {
        uint c = iban[i];
        bool match = pattern[i] switch
        {
            'n' => c is >= '0' and <= '9',
            'a' => c is >= 'A' and <= 'Z',
            'c' => c is >= '0' and <= '9' or >= 'A' and <= 'Z',
            var t => t == c,
        };

        if (!match)
        {
            return null;
        }
    }

    return iban;
}

This issue concerns the single-range expressions in the 'n' and 'a' arms, not optimization of the two disjoint ranges in the 'c' arm.

In the 'c' arm, the fix optimizes the digit range, but the letter range still uses separate comparisons because its lower-bound failure proceeds to checking digits while its upper-bound failure rejects the character.

An equivalent helper for these constant bounds is:

private static bool IsBetween(uint value, uint lower, uint upper)
    => unchecked(value - lower) <= upper - lower;

Replacing the 'n' and 'a' arm expressions with calls to this helper allows each range check to use a subtraction and one unsigned comparison. Where profitable, the relational patterns should produce similarly efficient native code without requiring manual arithmetic.

Configuration

  • Windows 11, x64.
  • Application compiled in Release with SDK 11.0.100-rc.1.26425.128, targeting net11.0.
  • Executed on an isolated main-line runtime pack, 12.0.0-alpha.1.26509.106 (dotnet/dotnet commit 4fd6fbb479abbacd43b32fdc0ef6f68414cc028d), with its JIT replaced by the Release x64 JIT from #135421's CI build.

Regression?

Not known to be a regression.

Data

With the fixed JIT:

Case Observed native code
Standalone bool IsDigit(uint c) => c is >= '0' and <= '9' Subtraction and one unsigned comparison
Parser's 'n' arm Separate comparisons against 48 and 57
Parser's 'a' arm Separate comparisons against 65 and 90
Parser's 'c' digit range Subtraction and comparison against 9
Parser's 'c' letter range Separate comparisons

For example, the numeric arm in warmed-up Tier1 code still contains:

       cmp      r10d, 48
       jb       BelowRange
       cmp      r10d, 57
       setbe    r11b
       movzx    r11, r11b

The standalone digit predicate instead contains:

       sub      ecx, 48
       cmp      ecx, 9
       setbe    al
       movzx    rax, al
       ret

Analysis

The IL produced by Roslyn differs in how the Boolean result is consumed.

For the 'n' arm, the captured IL has the following shape. Labels and local names below are symbolic, and unrelated switch arms are omitted:

    ldloc c
    ldc.i4.s 48
    blt.un.s BelowRange

    ldloc c
    ldc.i4.s 57
    cgt.un
    ldc.i4.0
    ceq
    br.s StoreResult

BelowRange:
    ldc.i4.0

StoreResult:
    stloc match
    br.s TestResult

// Other switch arms also assign match.

TestResult:
    ldloc match
    brtrue.s Continue
    ldnull
    ret

The lower-bound comparison branches. The upper-bound comparison produces a Boolean value, which is stored as the switch result. A later instruction branches on that stored result. The 'a' arm has the same structure with bounds 65 and 90.

By comparison, the standalone predicate addressed by #135418:

public static bool IsDigit(uint c)
    => c is >= '0' and <= '9';

has this IL:

    ldarg.0
    ldc.i4.s 48
    blt.un.s ReturnFalse

    ldarg.0
    ldc.i4.s 57
    cgt.un
    ldc.i4.0
    ceq
    ret

ReturnFalse:
    ldc.i4.0
    ret

Both forms contain a branch for the lower bound and a value-producing comparison for the upper bound. The fixed example returns the Boolean directly; the parser stores it as a switch-expression result and tests it afterward.

Note

This issue was drafted and its reproduction investigated with GitHub Copilot.

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Oct 12, 2026
  2. dotnet-policy-service commented on Oct 12, 2026

    @dotnet-policy-service
    Contributor

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

  3. added this to the Future milestone on Oct 12, 2026
  4. removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMItenet-performancePerformance related issue

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions