Skip to content

Commit

Permalink
Remove reflection in ValueType.Equals/GetHashCode (dotnet#5436)
Browse files Browse the repository at this point in the history
The implementation relies on a new virtual method on `System.ValueType` that provides information about fields on a type. An override of this method is injected by the compiler when needed.

This implementation is different from what Project N does (where we inject actual overrides of both methods). The CoreRT implementation is a bit more space-saving because there is only one method that does fewer things.

This ends up being a 0.9% regression in size of a hello world app, but we do get some correctness with it and a potential to get the reflection stack completely out of the base hello world image (that one will be a huge win). We can get some of the regression back by:

* Getting RyuJIT to generate more efficient code for the offset calculation (dotnet/coreclr#16527)
* Making fewer things reflectable. We're currently generating the data for e.g. OSVERSIONINFO because it's used as a parameter in a method that is reflectable, and therefore the parameter gets boxed, and therefore it needs this data. It also drags in a fixed buffer internal type because it's a valuetype field on OSVERSIONINFO. This alone costs us 100+ bytes.
  • Loading branch information
MichalStrehovsky authored and Konstantin Baladurin committed Mar 15, 2018
1 parent 8bbdab5 commit b442bed
Show file tree
Hide file tree
Showing 7 changed files with 535 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 Internal.TypeSystem;

namespace Internal.IL.Stubs
{
partial class ValueTypeGetFieldHelperMethodOverride
{
protected internal override int ClassCode => 2036839816;

protected internal override int CompareToImpl(MethodDesc other, TypeSystemComparer comparer)
{
var otherMethod = (ValueTypeGetFieldHelperMethodOverride)other;

return comparer.Compare(_owningType, otherMethod._owningType);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// 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.Collections.Generic;

using Internal.TypeSystem;

namespace Internal.IL.Stubs
{
/// <summary>
/// Synthetic method override of "int ValueType.__GetFieldHelper(Int32, out EETypePtr)". This method is injected
/// into all value types that cannot have their Equals(object) and GetHashCode() methods operate on individual
/// bytes. The purpose of the override is to provide access to the value types' fields and their types.
/// </summary>
public sealed partial class ValueTypeGetFieldHelperMethodOverride : ILStubMethod
{
private DefType _owningType;
private MethodSignature _signature;

internal ValueTypeGetFieldHelperMethodOverride(DefType owningType)
{
_owningType = owningType;
}

public override TypeSystemContext Context
{
get
{
return _owningType.Context;
}
}

public override TypeDesc OwningType
{
get
{
return _owningType;
}
}

public override MethodSignature Signature
{
get
{
if (_signature == null)
{
TypeSystemContext context = _owningType.Context;
TypeDesc int32Type = context.GetWellKnownType(WellKnownType.Int32);
TypeDesc eeTypePtrType = context.SystemModule.GetKnownType("System", "EETypePtr");

_signature = new MethodSignature(0, 0, int32Type, new[] {
int32Type,
eeTypePtrType.MakeByRefType()
});
}

return _signature;
}
}

public override MethodIL EmitIL()
{
TypeDesc owningType = _owningType.InstantiateAsOpen();

ILEmitter emitter = new ILEmitter();

TypeDesc eeTypePtrType = Context.SystemModule.GetKnownType("System", "EETypePtr");
MethodDesc eeTypePtrOfMethod = eeTypePtrType.GetKnownMethod("EETypePtrOf", null);
ILToken eeTypePtrToken = emitter.NewToken(eeTypePtrType);

var switchStream = emitter.NewCodeStream();
var getFieldStream = emitter.NewCodeStream();

ArrayBuilder<ILCodeLabel> fieldGetters = new ArrayBuilder<ILCodeLabel>();
foreach (FieldDesc field in owningType.GetFields())
{
if (field.IsStatic)
continue;

ILCodeLabel label = emitter.NewCodeLabel();
fieldGetters.Add(label);

getFieldStream.EmitLabel(label);
getFieldStream.EmitLdArg(2);

// We need something we can instantiate EETypePtrOf over. Also, the classlib
// code doesn't handle pointers.
TypeDesc boxableFieldType = field.FieldType;
if (boxableFieldType.IsPointer || boxableFieldType.IsFunctionPointer)
boxableFieldType = Context.GetWellKnownType(WellKnownType.IntPtr);

MethodDesc ptrOfField = eeTypePtrOfMethod.MakeInstantiatedMethod(boxableFieldType);
getFieldStream.Emit(ILOpcode.call, emitter.NewToken(ptrOfField));

getFieldStream.Emit(ILOpcode.stobj, eeTypePtrToken);

getFieldStream.EmitLdArg(0);
getFieldStream.Emit(ILOpcode.ldflda, emitter.NewToken(field));

getFieldStream.EmitLdArg(0);

getFieldStream.Emit(ILOpcode.sub);

getFieldStream.Emit(ILOpcode.ret);
}

if (fieldGetters.Count > 0)
{
switchStream.EmitLdArg(1);
switchStream.EmitSwitch(fieldGetters.ToArray());
}

switchStream.EmitLdc(fieldGetters.Count);

switchStream.Emit(ILOpcode.ret);

return emitter.Link(this);
}

public override Instantiation Instantiation
{
get
{
return Instantiation.Empty;
}
}

public override bool IsVirtual
{
get
{
return true;
}
}

public override string Name
{
get
{
return "__GetFieldHelper";
}
}
}
}
189 changes: 189 additions & 0 deletions src/Common/src/TypeSystem/IL/TypeSystemContext.ValueTypeMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// 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.Collections.Generic;

using Internal.IL.Stubs;

using Debug = System.Diagnostics.Debug;

namespace Internal.TypeSystem
{
public abstract partial class TypeSystemContext
{
private MethodDesc _objectEqualsMethod;

private class ValueTypeMethodHashtable : LockFreeReaderHashtable<DefType, MethodDesc>
{
protected override int GetKeyHashCode(DefType key) => key.GetHashCode();
protected override int GetValueHashCode(MethodDesc value) => value.OwningType.GetHashCode();
protected override bool CompareKeyToValue(DefType key, MethodDesc value) => key == value.OwningType;
protected override bool CompareValueToValue(MethodDesc v1, MethodDesc v2) => v1.OwningType == v2.OwningType;

protected override MethodDesc CreateValueFromKey(DefType key)
{
return new ValueTypeGetFieldHelperMethodOverride(key);
}
}

private ValueTypeMethodHashtable _valueTypeMethodHashtable = new ValueTypeMethodHashtable();

protected virtual IEnumerable<MethodDesc> GetAllMethodsForValueType(TypeDesc valueType)
{
TypeDesc valueTypeDefinition = valueType.GetTypeDefinition();

if (RequiresGetFieldHelperMethod((MetadataType)valueTypeDefinition))
{
MethodDesc getFieldHelperMethod = _valueTypeMethodHashtable.GetOrCreateValue((DefType)valueTypeDefinition);

// Check that System.ValueType has the method we're overriding.
Debug.Assert(valueTypeDefinition.BaseType.GetMethod(getFieldHelperMethod.Name, null) != null);

if (valueType != valueTypeDefinition)
{
yield return GetMethodForInstantiatedType(getFieldHelperMethod, (InstantiatedType)valueType);
}
else
{
yield return getFieldHelperMethod;
}
}

foreach (MethodDesc method in valueType.GetMethods())
yield return method;
}

private bool RequiresGetFieldHelperMethod(MetadataType valueType)
{
if (_objectEqualsMethod == null)
_objectEqualsMethod = GetWellKnownType(WellKnownType.Object).GetMethod("Equals", null);

// If the classlib doesn't have Object.Equals, we don't need this.
if (_objectEqualsMethod == null)
return false;

// Byref-like valuetypes cannot be boxed.
if (valueType.IsByRefLike)
return false;

// Enums get their overrides from System.Enum.
if (valueType.IsEnum)
return false;

return !CanCompareValueTypeBits(valueType);
}

private bool CanCompareValueTypeBits(MetadataType type)
{
Debug.Assert(type.IsValueType);

if (type.ContainsGCPointers)
return false;

if (type.IsGenericDefinition)
return false;

OverlappingFieldTracker overlappingFieldTracker = new OverlappingFieldTracker(type);

bool result = true;
foreach (var field in type.GetFields())
{
if (field.IsStatic)
continue;

if (!overlappingFieldTracker.TrackField(field))
{
// This field overlaps with another field - can't compare memory
result = false;
break;
}

TypeDesc fieldType = field.FieldType;
if (fieldType.IsPrimitive || fieldType.IsEnum || fieldType.IsPointer || fieldType.IsFunctionPointer)
{
TypeFlags category = fieldType.UnderlyingType.Category;
if (category == TypeFlags.Single || category == TypeFlags.Double)
{
// Double/Single have weird behaviors around negative/positive zero
result = false;
break;
}
}
else
{
// Would be a suprise if this wasn't a valuetype. We checked ContainsGCPointers above.
Debug.Assert(fieldType.IsValueType);

// If the field overrides Equals, we can't use the fast helper because we need to call the method.
if (fieldType.FindVirtualFunctionTargetMethodOnObjectType(_objectEqualsMethod).OwningType == type)
{
result = false;
break;
}

if (!CanCompareValueTypeBits((MetadataType)fieldType))
{
result = false;
break;
}
}
}

// If there are gaps, we can't memcompare
if (result && overlappingFieldTracker.HasGaps)
result = false;

return result;
}

private struct OverlappingFieldTracker
{
private bool[] _usedBytes;

public OverlappingFieldTracker(MetadataType type)
{
_usedBytes = new bool[type.InstanceFieldSize.AsInt];
}

public bool TrackField(FieldDesc field)
{
int fieldBegin = field.Offset.AsInt;

TypeDesc fieldType = field.FieldType;

int fieldEnd;
if (fieldType.IsPointer || fieldType.IsFunctionPointer)
{
fieldEnd = fieldBegin + field.Context.Target.PointerSize;
}
else
{
Debug.Assert(fieldType.IsValueType);
fieldEnd = fieldBegin + ((DefType)fieldType).InstanceFieldSize.AsInt;
}

for (int i = fieldBegin; i < fieldEnd; i++)
{
if (_usedBytes[i])
return false;
_usedBytes[i] = true;
}

return true;
}

public bool HasGaps
{
get
{
for (int i = 0; i < _usedBytes.Length; i++)
if (!_usedBytes[i])
return true;

return false;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ protected override IEnumerable<MethodDesc> GetAllMethods(TypeDesc type)
{
return GetAllMethodsForEnum(type);
}
else if (type.IsValueType)
{
return GetAllMethodsForValueType(type);
}

return type.GetMethods();
}
Expand Down
9 changes: 9 additions & 0 deletions src/ILCompiler.TypeSystem/src/ILCompiler.TypeSystem.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@
<Compile Include="..\..\Common\src\TypeSystem\IL\Stubs\StructMarshallingThunk.Sorting.cs">
<Link>IL\Stubs\StructMarshallingThunk.Sorting.cs</Link>
</Compile>
<Compile Include="..\..\Common\src\TypeSystem\IL\Stubs\ValueTypeGetFieldHelperMethodOverride.cs">
<Link>IL\Stubs\ValueTypeGetFieldHelperMethodOverride.cs</Link>
</Compile>
<Compile Include="..\..\Common\src\TypeSystem\IL\Stubs\ValueTypeGetFieldHelperMethodOverride.Sorting.cs">
<Link>IL\Stubs\ValueTypeGetFieldHelperMethodOverride.Sorting.cs</Link>
</Compile>
<Compile Include="..\..\Common\src\TypeSystem\IL\TypeSystemContext.DelegateInfo.cs">
<Link>IL\TypeSystemContext.DelegateInfo.cs</Link>
</Compile>
Expand Down Expand Up @@ -418,6 +424,9 @@
<Compile Include="..\..\Common\src\TypeSystem\IL\TypeSystemContext.EnumMethods.cs">
<Link>IL\TypeSystemContext.EnumMethods.cs</Link>
</Compile>
<Compile Include="..\..\Common\src\TypeSystem\IL\TypeSystemContext.ValueTypeMethods.cs">
<Link>IL\TypeSystemContext.ValueTypeMethods.cs</Link>
</Compile>
<Compile Include="..\..\Common\src\TypeSystem\Interop\IL\InlineArrayType.Sorting.cs">
<Link>TypeSystem\Interop\IL\InlineArrayType.Sorting.cs</Link>
</Compile>
Expand Down
Loading

0 comments on commit b442bed

Please sign in to comment.