Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

[WIP] Fast codegen-free field access #23783

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/System.Private.CoreLib/System.Private.CoreLib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@
<Compile Include="$(BclSourcesRoot)\System\Reflection\Emit\TypeBuilder.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\Emit\TypeBuilderInstantiation.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\Emit\XXXOnTypeBuilderInstantiation.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\FieldAccessor.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\ReadOnlyFieldAccessor.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\FieldInfo.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\INVOCATION_FLAGS.cs" />
<Compile Include="$(BclSourcesRoot)\System\Reflection\LoaderAllocator.cs" />
Expand Down
36 changes: 36 additions & 0 deletions src/System.Private.CoreLib/src/System/Reflection/FieldAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.CompilerServices;
using Internal.Runtime.CompilerServices;

namespace System.Reflection
{
public class FieldAccessor<TObject, TField> : ReadOnlyFieldAccessor<TObject, TField>
{
public FieldAccessor(FieldInfo fieldInfo) : base(fieldInfo)
{
// We perform two additioanl checks above and beyond the base type's ctor:
// - The field must not be marked readonly
// - The field's type must exactly match TField

if (fieldInfo.IsInitOnly)
{
// TODO: Use a better resource string for this.
throw new MissingMemberException(SR.MissingMemberTypeRef);
}

if (fieldInfo.FieldType != typeof(TField))
{
// TODO: Use a better resource string for this.
throw new MissingMemberException(SR.MissingMemberTypeRef);
}
}

// Delegate to the optimized base implementation, then reinterpret the returned
// ref as a mutable ref instead of an immutable ref.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new ref TField GetRef(ref TObject obj) => ref Unsafe.AsRef(in base.GetRef(in obj));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics;
using System.Runtime.CompilerServices;
using Internal.Runtime.CompilerServices;

namespace System.Reflection
{
public class ReadOnlyFieldAccessor<TObject, TField>
{
private readonly IntPtr _fieldOffset;

public ReadOnlyFieldAccessor(FieldInfo fieldInfo)
{
// There are four checks we perform:
// - The field must be a regular RtFieldInfo (not a manufactured FieldInfo)
// - The field must be an instance field
// - The field must be declared on TObject or a superclass
// - The field's type must be reinterpret_castable to TField

if (fieldInfo is null)
{
throw new ArgumentNullException(nameof(fieldInfo));
}

if (!(fieldInfo is RtFieldInfo rtFieldInfo))
{
throw new ArgumentException(SR.Argument_MustBeRuntimeFieldInfo, nameof(fieldInfo));
}

if (rtFieldInfo.IsStatic)
{
// TODO: Use a better resource string for this.
throw new ArgumentException(SR.Format(SR.Argument_TypedReferenceInvalidField, rtFieldInfo.Name));
}

Type fieldDeclaringType = rtFieldInfo.GetDeclaringTypeInternal();
Debug.Assert(fieldDeclaringType != null);

if (typeof(TObject) != fieldDeclaringType && !typeof(TObject).IsSubclassOf(fieldDeclaringType))
{
// TODO: Use a better resource string for this.
throw new MissingMemberException(SR.MissingMemberTypeRef);
}

Type fieldType = rtFieldInfo.FieldType;
Debug.Assert(fieldType != null);

if (fieldType.IsValueType)
{
// For value types, TField must exactly match the FieldInfo's actual type
// since there's no superclass hierarchy we're able to reinterpret_cast to.

if (typeof(TField) != fieldType)
{
// TODO: Use a better resource string for this.
throw new MissingMemberException(SR.MissingMemberTypeRef);
}
}
else
{
// For reference types, we only care that the FieldInfo's actual type can
// be reinterpret_cast to TObject. For example, it'll be valid to reinterpret_cast
// to 'object' or to an interface that the FieldInfo's actual type implements.

if (!typeof(TField).IsAssignableFrom(fieldType))
{
// TODO: Use a better resource string for this.
throw new MissingMemberException(SR.MissingMemberTypeRef);
}
}

_fieldOffset = rtFieldInfo.GetOffsetInBytes();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref readonly TField GetRef(in TObject obj)
{
// Normally, the JIT would perform a null check on "this" before invoking the instance method.
// By moving the this._fieldOffset dereference to the beginning of the method and storing it
// as a local copy, the JIT is able to elide the earlier null check because the line immediately
// following would cause the correct NullReferenceException if the caller called (null).GetRef(...).

IntPtr fieldOffset = _fieldOffset;

if (RuntimeHelpers.IsReference<TObject>())
{
// For reference types, we need to dereference the 'in' parameter before we can get the
// address of the first field in the object. The call to GetRawData below will also perform
// a null check on the 'obj' parameter. This null check can be elided entirely if the JIT
// has other ways of proving that 'obj' cannot be null at the entry to this method.

#nullable disable // we want to incur the NRE in the line below
ref byte rawDataRef = ref obj.GetRawData();
#nullable restore

// The field offset isn't from the start of the object header; it's from the start
// of the first data field in the object (returned by GetRawData above).

return ref Unsafe.As<byte, TField>(ref Unsafe.AddByteOffset(ref rawDataRef, fieldOffset));
}
else
{
// Perform our arithmetic with 'byte' instead of reinterpret_casting straight from
// TObject to TField, otherwise a debugger stepping through this code could exhibit odd
// behavior since it might misinterpret the actual type of the underlying refs.

ref byte rawDataRef = ref Unsafe.As<TObject, byte>(ref Unsafe.AsRef(in obj));

// For value types, we only dereference the 'in' parameter to check that it's not null,
// but we otherwise don't care about the data. This check can be elided entirely if the
// JIT has other ways of proving that 'obj' cannot be a null ref at the entry to this method.

if (rawDataRef == default)
{
// intentionally left blank - we only care about forcing the null check
}

return ref Unsafe.As<byte, TField>(ref Unsafe.AddByteOffset(ref rawDataRef, fieldOffset));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ internal override bool CacheEquals(object? o)
return o is RtFieldInfo m && m.m_fieldHandle == m_fieldHandle;
}

// Returns the offset (in bytes) from the first field of the object to the data represented by this field.
// The object's first field is at offset 0; the object's method table is at offset -IntPtr.Size.
internal IntPtr GetOffsetInBytes()
{
return (IntPtr)(byte*)RuntimeFieldHandle.GetFieldOffset(new RuntimeFieldHandleInternal(m_fieldHandle));
}
#endregion

#region MemberInfo Overrides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ internal static void ExecuteBackoutCodeHelper(object backoutCode, object? userDa
((CleanupCode)backoutCode)(userData, exceptionThrown);
}

/// <returns>true if given type is reference type;
/// false if given type is value type (even if the value type contains references)</returns>
[Intrinsic]
internal static bool IsReference<T>()
{
// The body of this function will be replaced by the EE with unsafe code!!!
// See getILIntrinsicImplementationForRuntimeHelpers for how this happens.
throw new InvalidOperationException();
}

/// <returns>true if given type is reference type or value type that contains references</returns>
[Intrinsic]
public static bool IsReferenceOrContainsReferences<T>()
Expand Down
3 changes: 3 additions & 0 deletions src/System.Private.CoreLib/src/System/RuntimeHandles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,9 @@ internal static RuntimeType GetApproxDeclaringType(IRuntimeFieldInfo field)
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern bool AcquiresContextFromThis(RuntimeFieldHandleInternal field);

[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern uint GetFieldOffset(RuntimeFieldHandleInternal field);

// ISerializable interface
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Expand Down
1 change: 1 addition & 0 deletions src/vm/ecalllist.h
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ FCFuncStart(gCOMFieldHandleNewFuncs)
FCFuncElement("GetToken", RuntimeFieldHandle::GetToken)
FCFuncElement("GetStaticFieldForGenericType", RuntimeFieldHandle::GetStaticFieldForGenericType)
FCFuncElement("AcquiresContextFromThis", RuntimeFieldHandle::AcquiresContextFromThis)
FCFuncElement("GetFieldOffset", RuntimeFieldHandle::GetFieldOffset)
FCFuncEnd()


Expand Down
28 changes: 28 additions & 0 deletions src/vm/jitinterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7472,6 +7472,34 @@ bool getILIntrinsicImplementationForRuntimeHelpers(MethodDesc * ftn,

mdMethodDef tk = ftn->GetMemberDef();

if (tk == MscorlibBinder::GetMethod(METHOD__RUNTIME_HELPERS__IS_REFERENCE)->GetMemberDef())
{
_ASSERTE(ftn->HasMethodInstantiation());
Instantiation inst = ftn->GetMethodInstantiation();

_ASSERTE(ftn->GetNumGenericMethodArgs() == 1);
TypeHandle typeHandle = inst[0];
MethodTable* methodTable = typeHandle.GetMethodTable();

static const BYTE returnTrue[] = { CEE_LDC_I4_1, CEE_RET };
static const BYTE returnFalse[] = { CEE_LDC_I4_0, CEE_RET };

if (!methodTable->IsValueType())
{
methInfo->ILCode = const_cast<BYTE*>(returnTrue);
}
else
{
methInfo->ILCode = const_cast<BYTE*>(returnFalse);
}

methInfo->ILCodeSize = sizeof(returnTrue);
methInfo->maxStack = 1;
methInfo->EHcount = 0;
methInfo->options = (CorInfoOptions)0;
return true;
}

if (tk == MscorlibBinder::GetMethod(METHOD__RUNTIME_HELPERS__IS_REFERENCE_OR_CONTAINS_REFERENCES)->GetMemberDef())
{
_ASSERTE(ftn->HasMethodInstantiation());
Expand Down
1 change: 1 addition & 0 deletions src/vm/mscorlib.h
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ DEFINE_CLASS(RUNTIME_HELPERS, CompilerServices, RuntimeHelpers)
DEFINE_METHOD(RUNTIME_HELPERS, PREPARE_CONSTRAINED_REGIONS, PrepareConstrainedRegions, SM_RetVoid)
DEFINE_METHOD(RUNTIME_HELPERS, PREPARE_CONSTRAINED_REGIONS_NOOP, PrepareConstrainedRegionsNoOP, SM_RetVoid)
DEFINE_METHOD(RUNTIME_HELPERS, EXECUTE_BACKOUT_CODE_HELPER, ExecuteBackoutCodeHelper, SM_Obj_Obj_Bool_RetVoid)
DEFINE_METHOD(RUNTIME_HELPERS, IS_REFERENCE, IsReference, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, IS_REFERENCE_OR_CONTAINS_REFERENCES, IsReferenceOrContainsReferences, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, IS_BITWISE_EQUATABLE, IsBitwiseEquatable, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, GET_RAW_SZ_ARRAY_DATA, GetRawSzArrayData, NoSig)
Expand Down
13 changes: 13 additions & 0 deletions src/vm/runtimehandles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,19 @@ FCIMPL1(FC_BOOL_RET, RuntimeFieldHandle::AcquiresContextFromThis, FieldDesc *pFi
}
FCIMPLEND

FCIMPL1(UINT32, RuntimeFieldHandle::GetFieldOffset, FieldDesc* pField)
{
CONTRACTL{
FCALL_CHECK;
PRECONDITION(CheckPointer(pField));
}
CONTRACTL_END;

return pField->GetOffset();

}
FCIMPLEND

FCIMPL1(ReflectModuleBaseObject*, RuntimeTypeHandle::GetModule, ReflectClassBaseObject *pTypeUNSAFE) {
CONTRACTL {
FCALL_CHECK;
Expand Down
1 change: 1 addition & 0 deletions src/vm/runtimehandles.h
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ class RuntimeFieldHandle {
static FCDECL1(INT32, GetToken, ReflectFieldObject *pFieldUNSAFE);
static FCDECL2(FieldDesc*, GetStaticFieldForGenericType, FieldDesc *pField, ReflectClassBaseObject *pDeclaringType);
static FCDECL1(FC_BOOL_RET, AcquiresContextFromThis, FieldDesc *pField);
static FCDECL1(UINT32, GetFieldOffset, FieldDesc* pField);
};

class ModuleHandle {
Expand Down