JIT does not merge consecutive comparisons against ranges known to be in bounds
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
- Domain
- compilers, performance
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
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/runtime
-
agentic-workflows untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
area-System.Reflection blocking-clean-ci-optional Known Build Error os-mac-os-x untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
area-CodeGen-coreclr untriaged
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
agentic-workflows untriaged
Difficulty 1/5 Under an hour Newbie friendliness 78/100
-
area-VM-meta-mono untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SubtitleEdit/subtitleedit#15108 · 1 comment ·
-
area/docs-content Bug pulumi/docs
Difficulty 1/5 1-3 hours Newbie friendliness 94/100
-
Create parent directories only after the containment check in InstallHelper.TryExtractToDirectory Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
PowerShell/PSResourceGet#2056 ·