Repository navigation
Codegen: Bound check not elided in ref struct Span field index access #72004
Copy link
Copy link
Open
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Milestone
Description
Repro:
internal ref struct Foo
{
private readonly Span<int> _span;
internal Foo(Span<int> span) => _span = span;
internal void Set(int index)
{
if ((uint)index < (uint)_span.Length)
{
_span[index] = 42;
}
}
}Codegen (x64):
sub rsp,38
xor eax,eax
mov [rsp+28],rax
mov [rsp+30],rax
mov rax,[rcx+8]
test rax,rax
je short M00_L02
lea rdx,[rax+10]
mov eax,[rax+8]
M00_L00:
lea rcx,[rsp+28]
mov [rcx],rdx
mov [rcx+8],eax
cmp dword ptr [rsp+30],3
jbe short M00_L01
lea rdx,[rsp+28]
cmp dword ptr [rdx+8],3 ; redundant check for bound-check -- should be elided
jbe short M00_L03
mov rax,[rdx]
mov dword ptr [rax+0C],2A
M00_L01:
add rsp,38
ret
M00_L02:
xor edx,edx
xor eax,eax
jmp short M00_L00
M00_L03:
call CORINFO_HELP_RNGCHKFAIL
int 3
; Total bytes of code 88Workaround: fetch the span to a local.
+Span<int> span = _span;
-if ((uint)index < (uint)_span.Length)
+if ((uint)index < (uint)span.Length)
{
- _span[index] = 42;
+ span[index] = 42;
}When the index is not constant (here 3), the same issue occurs.
Complete listing of code (C#)
using BenchmarkDotNet.Attributes;
BenchmarkDotNet.Running.BenchmarkRunner.Run<Bench>();
[DisassemblyDiagnoser]
[ShortRunJob]
public class Bench
{
private readonly int[] _array = new int[100];
[Benchmark]
public void NoLocalSpan()
{
Foo foo = new(_array);
foo.Set(3);
}
[Benchmark]
public void LocalSpan()
{
Foo1 foo = new(_array);
foo.Set(3);
}
}
internal ref struct Foo
{
private readonly Span<int> _span;
internal Foo(Span<int> span) => _span = span;
internal void Set(int index)
{
if ((uint)index < (uint)_span.Length)
{
_span[index] = 42;
}
}
}
internal ref struct Foo1
{
private readonly Span<int> _span;
internal Foo1(Span<int> span) => _span = span;
internal void Set(int index)
{
Span<int> span = _span;
if ((uint)index < (uint)span.Length)
{
span[index] = 42;
}
}
}Complete listing of generated code
; Bench.NoLocalSpan()
sub rsp,38
xor eax,eax
mov [rsp+28],rax
mov [rsp+30],rax
mov rax,[rcx+8]
test rax,rax
je short M00_L02
lea rdx,[rax+10]
mov eax,[rax+8]
M00_L00:
lea rcx,[rsp+28]
mov [rcx],rdx
mov [rcx+8],eax
cmp dword ptr [rsp+30],3
jbe short M00_L01
lea rdx,[rsp+28]
cmp dword ptr [rdx+8],3
jbe short M00_L03
mov rax,[rdx]
mov dword ptr [rax+0C],2A
M00_L01:
add rsp,38
ret
M00_L02:
xor edx,edx
xor eax,eax
jmp short M00_L00
M00_L03:
call CORINFO_HELP_RNGCHKFAIL
int 3
; Total bytes of code 88
; Bench.LocalSpan()
sub rsp,18
xor eax,eax
mov [rsp+8],rax
mov [rsp+10],rax
mov rax,[rcx+8]
test rax,rax
je short M00_L02
lea rdx,[rax+10]
mov eax,[rax+8]
M00_L00:
lea rcx,[rsp+8]
mov [rcx],rdx
mov [rcx+8],eax
lea rdx,[rsp+8]
mov rax,[rdx]
mov edx,[rdx+8]
cmp edx,3
jbe short M00_L01
mov dword ptr [rax+0C],2A
M00_L01:
add rsp,18
ret
M00_L02:
xor edx,edx
xor eax,eax
jmp short M00_L00
; Total bytes of code 77Cf. #67448 (comment)
category:cq
theme:bounds-checks
skill-level:intermediate
cost:small
impact:small
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Activity
EgorBo commentedon Jul 12, 2022
I don't see any bound checks in Main:

gfoidl commentedon Jul 12, 2022
Hm, interesting. Tried it with https://github.com/dotnet/installer version 7.0.100-preview7.22362.1 (win-x64, current main as of now), and there is a bound-check.
Was there a recent change in JIT? How does the codegen look when this method is called -- it's small enough to be inlined, then I saw the bound-check. Didn't look in isolation to that method.
EgorBo commentedon Jul 12, 2022
EgorBo commentedon Jul 12, 2022
hm.. actually it might be PGO... 🤔
EgorBo commentedon Jul 12, 2022
So, in some cases JIT doesn't eliminate bound checks inside cold blocks - it's pretty normal behavior to me. So an example where it happens in non-cold block would be nice to have.
Example:
Here is the codegen for
Setin Tier1 with PGO enabled:Now let me change the call from
f.Set(1)tof.Set(100)so we will never step into the condition inside Set, the codegen will be:A stale/generic PGO might bake profile that doesn't work well for currently tested app and can mess it up but presumably it's a rare case. cc @AndyAyersMS
gfoidl commentedon Jul 12, 2022
This is what I'd except too. But when run via BenchmarkDotNet it should be hot enough to get "optimized" Tier-1 code.
Also "NoInlining" isn't like real-world usage here (I know you know this, of course).
May this be related to something in BDN and a preview version of .NET?
EgorBo commentedon Jul 12, 2022
Ok, I think I can reproduce yeah