Skip to content

Commit

Permalink
Convert fast path of ValueType.GetHashCode to managed (#97590)
Browse files Browse the repository at this point in the history
* Access CanCompareBits from MethodTableAuxiliaryData

* Switch to QCall

* Fast path of GetHashCode

* Call for RegularGetValueTypeHashCode

* Cleanup unused FCall

* Use HashCode type

* Handle recursive case inside native code

* Remove Double and Single from CoreLibBinder

* Apply suggestions from code review

Co-authored-by: Jan Kotas <jkotas@microsoft.com>

---------

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
  • Loading branch information
huoyaoyuan and jkotas authored Feb 21, 2024
1 parent 21c0940 commit 8cc7586
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 198 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,13 @@ internal unsafe struct MethodTable

// Additional conditional fields (see methodtable.h).
// m_pModule
// m_pAuxiliaryData

/// <summary>
/// A pointer to auxiliary data that is cold for method table.
/// </summary>
[FieldOffset(AuxiliaryDataOffset)]
public MethodTableAuxiliaryData* AuxiliaryData;

// union {
// m_pEEClass (pointer to the EE class)
// m_pCanonMT (pointer to the canonical method table)
Expand Down Expand Up @@ -523,6 +529,12 @@ internal unsafe struct MethodTable

private const int ParentMethodTableOffset = 0x10 + DebugClassNamePtr;

#if TARGET_64BIT
private const int AuxiliaryDataOffset = 0x20 + DebugClassNamePtr;
#else
private const int AuxiliaryDataOffset = 0x18 + DebugClassNamePtr;
#endif

#if TARGET_64BIT
private const int ElementTypeOffset = 0x30 + DebugClassNamePtr;
#else
Expand Down Expand Up @@ -610,6 +622,28 @@ public TypeHandle GetArrayElementTypeHandle()
public extern uint GetNumInstanceFieldBytes();
}

// Subset of src\vm\methodtable.h
[StructLayout(LayoutKind.Explicit)]
internal unsafe struct MethodTableAuxiliaryData
{
[FieldOffset(0)]
private uint Flags;

private const uint enum_flag_CanCompareBitsOrUseFastGetHashCode = 0x0001; // Is any field type or sub field type overrode Equals or GetHashCode
private const uint enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode = 0x0002; // Whether we have checked the overridden Equals or GetHashCode

public bool HasCheckedCanCompareBitsOrUseFastGetHashCode => (Flags & enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode) != 0;

public bool CanCompareBitsOrUseFastGetHashCode
{
get
{
Debug.Assert(HasCheckedCanCompareBitsOrUseFastGetHashCode);
return (Flags & enum_flag_CanCompareBitsOrUseFastGetHashCode) != 0;
}
}
}

/// <summary>
/// A type handle, which can wrap either a pointer to a <c>TypeDesc</c> or to a <see cref="MethodTable"/>.
/// </summary>
Expand Down
88 changes: 82 additions & 6 deletions src/coreclr/System.Private.CoreLib/src/System/ValueType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
**
===========================================================*/

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System
{
[Serializable]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public abstract class ValueType
public abstract partial class ValueType
{
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern",
Justification = "Trimmed fields don't make a difference for equality")]
Expand All @@ -36,7 +38,7 @@ public override unsafe bool Equals([NotNullWhen(true)] object? obj)

// if there are no GC references in this object we can avoid reflection
// and do a fast memcmp
if (CanCompareBits(this))
if (CanCompareBitsOrUseFastGetHashCode(RuntimeHelpers.GetMethodTable(obj))) // MethodTable kept alive by access to object below
{
return SpanHelpers.SequenceEqual(
ref RuntimeHelpers.GetRawData(this),
Expand Down Expand Up @@ -66,8 +68,23 @@ ref RuntimeHelpers.GetRawData(obj),
return true;
}

[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool CanCompareBits(object obj);
// Return true if the valuetype does not contain pointer, is tightly packed,
// does not have floating point number field and does not override Equals method.
private static unsafe bool CanCompareBitsOrUseFastGetHashCode(MethodTable* pMT)
{
MethodTableAuxiliaryData* pAuxData = pMT->AuxiliaryData;

if (pAuxData->HasCheckedCanCompareBitsOrUseFastGetHashCode)
{
return pAuxData->CanCompareBitsOrUseFastGetHashCode;
}

return CanCompareBitsOrUseFastGetHashCodeHelper(pMT);
}

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MethodTable_CanCompareBitsOrUseFastGetHashCode")]
[return: MarshalAs(UnmanagedType.Bool)]
private static unsafe partial bool CanCompareBitsOrUseFastGetHashCodeHelper(MethodTable* pMT);

/*=================================GetHashCode==================================
**Action: Our algorithm for returning the hashcode is a little bit complex. We look
Expand All @@ -79,8 +96,67 @@ ref RuntimeHelpers.GetRawData(obj),
**Arguments: None.
**Exceptions: None.
==============================================================================*/
[MethodImpl(MethodImplOptions.InternalCall)]
public extern override int GetHashCode();
public override unsafe int GetHashCode()
{
// The default implementation of GetHashCode() for all value types.
// Note that this implementation reveals the value of the fields.
// So if the value type contains any sensitive information it should
// implement its own GetHashCode().

MethodTable* pMT = RuntimeHelpers.GetMethodTable(this);
ref byte rawData = ref RuntimeHelpers.GetRawData(this);
HashCode hashCode = default;

// To get less colliding and more evenly distributed hash codes,
// we munge the class index into the hashcode
hashCode.Add((IntPtr)pMT);

if (CanCompareBitsOrUseFastGetHashCode(pMT))
{
// this is a struct with no refs and no "strange" offsets
uint size = pMT->GetNumInstanceFieldBytes();
hashCode.AddBytes(MemoryMarshal.CreateReadOnlySpan(ref rawData, (int)size));
}
else
{
object thisRef = this;
switch (GetHashCodeStrategy(pMT, ObjectHandleOnStack.Create(ref thisRef), out uint fieldOffset, out uint fieldSize))
{
case ValueTypeHashCodeStrategy.ReferenceField:
hashCode.Add(Unsafe.As<byte, object>(ref Unsafe.AddByteOffset(ref rawData, fieldOffset)).GetHashCode());
break;

case ValueTypeHashCodeStrategy.DoubleField:
hashCode.Add(Unsafe.As<byte, double>(ref Unsafe.AddByteOffset(ref rawData, fieldOffset)).GetHashCode());
break;

case ValueTypeHashCodeStrategy.SingleField:
hashCode.Add(Unsafe.As<byte, float>(ref Unsafe.AddByteOffset(ref rawData, fieldOffset)).GetHashCode());
break;

case ValueTypeHashCodeStrategy.FastGetHashCode:
Debug.Assert(fieldSize != 0);
hashCode.AddBytes(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AddByteOffset(ref rawData, fieldOffset), (int)fieldSize));
break;
}
}

return hashCode.ToHashCode();
}

// Must match the definition in src\vm\comutilnative.cpp
private enum ValueTypeHashCodeStrategy
{
None,
ReferenceField,
DoubleField,
SingleField,
FastGetHashCode,
}

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ValueType_GetHashCodeStrategy")]
private static unsafe partial ValueTypeHashCodeStrategy GetHashCodeStrategy(
MethodTable* pMT, ObjectHandleOnStack objHandle, out uint fieldOffset, out uint fieldSize);

public override string? ToString()
{
Expand Down
Loading

0 comments on commit 8cc7586

Please sign in to comment.