Skip to content

JIT doesn't elide some "this != null" checks when accessing fixed-size buffers in structs #49180

@GrabYourPitchforks

Description

@GrabYourPitchforks

When accessing fixed-sized buffers inside a struct's instance methods, the JIT sometimes unnecessarily includes defenses against the case where "this == nullptr". In one-off instance methods this isn't a huge problem, but when calling these instance methods within hot loops it bloats codegen and lowers throughput.

Repro code, demonstrating the problem outside of a loop:

using System;
using System.Runtime.CompilerServices;

public class C {
    private int _i;
    private MyStruct _mys;
    
    public void M(uint index) {
        var _ = _i; // force 'this' to be dereferenced, so JIT can propagate this != null
        Console.WriteLine(_mys.GetValue(index));
    }
    
    private unsafe struct MyStruct
    {
        private fixed byte Buffer[8];
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public byte GetValue(uint index)
        {
            if (index < 8)
            {
                return Buffer[index];
            }
            else
            {
                return 0;
            }
        }
    }
}

x64 codegen, courtesy SharpLab:

C.M(UInt32)
    L0000: mov eax, [rcx+8]  ; dereference this._i, which should allow JIT to propagate "this != nullptr" below
    L0003: add rcx, 0x10
    L0007: cmp edx, 8
    L000a: jae short L0016
    L000c: cmp [rcx], ecx  ; unnecessary check for "ref _mys.Buffer is <unaddressable>", which should be elided by L0000 above
    L000e: mov eax, edx
    L0010: movzx ecx, byte ptr [rcx+rax]
    L0014: jmp short L0018
    L0016: xor ecx, ecx
    L0018: jmp System.Console.WriteLine(Int32)

If the helper method is made an instance method of the class which has MyStruct as a field - rather than an instance method of MyStruct itself - the check is properly elided, as shown below.

using System;
using System.Runtime.CompilerServices;

public class D {
    private int _i;
    private MyStruct _mys;
    
    public void N(uint index)
    {
        var _ = _i; // force 'this' to be dereferenced, so JIT can propagate this != null
        Console.WriteLine(GetValue(index));
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private unsafe int GetValue(uint index)
    {
        if (index < 8)
        {
            return _mys.Buffer[index];
        }
        else
        {
            return 0;
        }
    }
    
    private unsafe struct MyStruct
    {
        internal fixed byte Buffer[8];
    }
}
D.N(UInt32)
    L0000: mov eax, [rcx+8]  ; deref this
    L0003: cmp edx, 8
    L0006: jae short L0011
    L0008: mov eax, edx
    L000a: movzx ecx, byte ptr [rcx+rax+0x10]  ; no additional guard needed before deref into _mys.Buffer
    L000f: jmp short L0013
    L0011: xor ecx, ecx
    L0013: jmp System.Console.WriteLine(Int32)

Moving this logic down is a viable workaround, but it harms code maintainability. It means that I can no longer keep the unsafe code wholly self-contained within the struct. It must instead be split across a few different types, and it leaks implementation details to callers who really shouldn't need to deal with unsafe concepts.

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions