JIT does not merge consecutive comparisons against ranges known to be in bounds

Open
#114,039 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Stale
Tech stack
csharp

Research direction

Start with the lowered C# form and the generated JIT assembly shown in the issue, then locate the JIT optimization area responsible for consecutive comparisons. The work is done when equivalent in-bounds comparisons produce fewer branches without changing the pattern-matching behavior; no source files or tests are named, so those must be identified first.

Written by the indexing model from the issue text.

Description

area-CodeGen-coreclr tenet-performance
Description

Given a method

static bool Test(ReadOnlySpan<byte> data)
{
    if (data.Length < 8)
        return false;

    return data
        is [0x01, 0x02, 0x03, 0x04, ..]
        or [0x05, 0x06, 0x07, 0x08, ..]
        or [0x09, 0x0A, 0x0B, 0x0C, ..]
        or [0x0D, 0x0E, 0x0F, 0x10, ..]
        or [0x11, 0x12, 0x13, 0x14, ..]
        or [0x15, 0x16, 0x17, 0x18, ..]
        or [0x19, 0x1A, 0x1B, 0x1C, ..];
}
and its lowered form

static bool Test(ReadOnlySpan<byte> data)
{
    if (data.Length < 8)
    {
        return false;
    }
    if (data.Length >= 4)
    {
        byte b = data[0];
        if ((uint)b <= 9u)
        {
            if (b != 1)
            {
                if (b != 5)
                {
                    if (b == 9 && data[1] == 10 && data[2] == 11 && data[3] == 12)
                    {
                        goto IL_01ad;
                    }
                }
                else if (data[1] == 6 && data[2] == 7 && data[3] == 8)
                {
                    goto IL_01ad;
                }
            }
            else if (data[1] == 2 && data[2] == 3 && data[3] == 4)
            {
                goto IL_01ad;
            }
        }
        else if ((uint)b <= 17u)
        {
            if (b != 13)
            {
                if (b == 17 && data[1] == 18 && data[2] == 19 && data[3] == 20)
                {
                    goto IL_01ad;
                }
            }
            else if (data[1] == 14 && data[2] == 15 && data[3] == 16)
            {
                goto IL_01ad;
            }
        }
        else if (b != 21)
        {
            if (b == 25 && data[1] == 26 && data[2] == 27 && data[3] == 28)
            {
                goto IL_01ad;
            }
        }
        else if (data[1] == 22 && data[2] == 23 && data[3] == 24)
        {
            goto IL_01ad;
        }
    }
    return false;
    IL_01ad:
    return true;
}

This results in a JIT output roughly like this:

Example:Test1(System.ReadOnlySpan`1[ubyte]):ubyte (FullOpts):
       push     rbp
       mov      rbp, rsp
       cmp      esi, 8
       jl       G_M57442_IG13
       movzx    rax, byte  ptr [rdi]
       cmp      eax, 9
       jle      SHORT G_M57442_IG08
       cmp      eax, 17
       jle      SHORT G_M57442_IG06
       cmp      eax, 21
       jne      SHORT G_M57442_IG05
       cmp      byte  ptr [rdi+0x01], 22
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x02], 23
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x03], 24
       je       G_M57442_IG11
G_M57442_IG04:  ;; offset=0x0035
       xor      eax, eax
       jmp      G_M57442_IG12
G_M57442_IG05:  ;; offset=0x003C
       cmp      eax, 25
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x01], 26
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x02], 27
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x03], 28
       jne      SHORT G_M57442_IG04
       jmp      G_M57442_IG11
G_M57442_IG06:  ;; offset=0x0058
       cmp      eax, 13
       jne      SHORT G_M57442_IG07
       cmp      byte  ptr [rdi+0x01], 14
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x02], 15
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x03], 16
       je       SHORT G_M57442_IG11
       jmp      SHORT G_M57442_IG04
G_M57442_IG07:  ;; offset=0x0071
       cmp      eax, 17
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x01], 18
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x02], 19
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x03], 20
       je       SHORT G_M57442_IG11
       jmp      SHORT G_M57442_IG04
G_M57442_IG08:  ;; offset=0x008A
       cmp      eax, 1
       jne      SHORT G_M57442_IG09
       cmp      byte  ptr [rdi+0x01], 2
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x02], 3
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x03], 4
       je       SHORT G_M57442_IG11
       jmp      SHORT G_M57442_IG04
G_M57442_IG09:  ;; offset=0x00A3
       cmp      eax, 5
       jne      SHORT G_M57442_IG10
       cmp      byte  ptr [rdi+0x01], 6
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x02], 7
       jne      SHORT G_M57442_IG04
       cmp      byte  ptr [rdi+0x03], 8
       je       SHORT G_M57442_IG11
       jmp      G_M57442_IG04
G_M57442_IG10:  ;; offset=0x00BF
       cmp      eax, 9
       jne      G_M57442_IG04
       cmp      byte  ptr [rdi+0x01], 10
       jne      G_M57442_IG04
       cmp      byte  ptr [rdi+0x02], 11
       jne      G_M57442_IG04
       cmp      byte  ptr [rdi+0x03], 12
       jne      G_M57442_IG04
G_M57442_IG11:  ;; offset=0x00E6
       mov      eax, 1
G_M57442_IG12:  ;; offset=0x00EB
       pop      rbp
       ret      
G_M57442_IG13:  ;; offset=0x00ED
       xor      eax, eax
       pop      rbp
       ret      
Analysis

Note how there are sequences of byte-by-byte comparisons in each block which end up jumping to the same destination. Because the implementation already asserts that the span length is greater than or equal 8, the compiler should be free to aggregate byte word comparisons into a larger ones in order to reduce branch count.

I have submitted a satellite issue https://github.com/dotnet/roslyn/issues/77908 regarding suboptimal lowering at Roslyn's end as well, but consecutive comparisons like above are quite popular regardless (especially in code ported from C) and, to my knowledge, there have already been PRs to improve this in the past versions.

Configuration

Whatever .NET 10 commit Godbolt is currently on

Regression?

No

Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/runtime

All issues in dotnet/runtime

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.