Skip to content

Span index access not eliminating bounds checks depending on access order #116088

Closed
@ovska

Description

@ovska

Description

When accessing span items by Index from end, some patterns do not eliminate the bounds check.

Reproduction Steps

Access a span by both its' first and last item. Both accesses incur an index bounds check, despite the s[0] ensuring the span is non-empty. Accessing s[^1] first eliminates the second bounds check.

IsEmpty and an explicit Length != 0 checks seem to work properly, e.g. s.IsEmpty ? -1 : s[0].

Expected behavior

public static int Bounds2(ReadOnlySpan<byte> s)
{
    byte sz = s[^1];
    byte s0 = s[0];
    return s0 + sz;
}
; Method ConsoleApp7.TestClass:Bounds2(System.ReadOnlySpan`1[ubyte]):int (FullOpts)
G_M000_IG01:                ;; offset=0x0000
       sub      rsp, 40

G_M000_IG02:                ;; offset=0x0004
       mov      rax, bword ptr [rcx]
       mov      ecx, dword ptr [rcx+0x08]
       lea      edx, [rcx-0x01]
       cmp      edx, ecx
       jae      SHORT G_M000_IG04
       mov      ecx, edx
       movzx    rcx, byte  ptr [rax+rcx]
       movzx    rax, byte  ptr [rax]
       add      eax, ecx

G_M000_IG03:                ;; offset=0x001C
       add      rsp, 40
       ret      

G_M000_IG04:                ;; offset=0x0021
       call     CORINFO_HELP_RNGCHKFAIL
       int3     
; Total bytes of code: 39

Actual behavior

Both accesses cause a range check.

public static int Bounds1(ReadOnlySpan<byte> s)
{
    byte s0 = s[0];
    byte sz = s[^1];
    return s0 + sz;
}
; Method ConsoleApp7.TestClass:Bounds1(System.ReadOnlySpan`1[ubyte]):int (FullOpts)
G_M000_IG01:                ;; offset=0x0000
       sub      rsp, 40

G_M000_IG02:                ;; offset=0x0004
       mov      rax, bword ptr [rcx]
       mov      ecx, dword ptr [rcx+0x08]
       test     ecx, ecx
       je       SHORT G_M000_IG04
       movzx    rdx, byte  ptr [rax]
       lea      r8d, [rcx-0x01]
       cmp      r8d, ecx
       jae      SHORT G_M000_IG04
       mov      ecx, r8d
       movzx    rax, byte  ptr [rax+rcx]
       add      eax, edx

G_M000_IG03:                ;; offset=0x0023
       add      rsp, 40
       ret      

G_M000_IG04:                ;; offset=0x0028
       call     CORINFO_HELP_RNGCHKFAIL
       int3     
; Total bytes of code: 46

Regression?

No response

Known Workarounds

Swap the operations or use unsafe code for the second access (which generates roughly the same ASM as the swapped version). This is obviously not a huge deal unless you are writing high-performance code without resorting to Unsafe & friends.

Configuration

dotnet --version: 9.0.203
dotnet build -f net9.0 -c Release -o bin\Release\net9.0\Disasmo-v5.9.2
DOTNET_TieredPGO=0
DOTNET_JitDisasmDiffable=0
DOTNET_TieredCompilation=0
DOTNET_ReadyToRun=1
DOTNET_TieredPGO_InstrumentOnlyHotCode=0
Windows 10 64bit ZEN2

Can also be seen on https://godbolt.org/z/4q66bo36n and on arm64 arch as well

Other information

No response

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions