-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
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
Type
Projects
Status