Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert fast path of ValueType.GetHashCode to managed #97590

Merged
merged 19 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,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 @@ -528,6 +534,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 @@ -615,6 +627,28 @@ public TypeHandle GetArrayElementTypeHandle()
public extern uint GetNumInstanceFieldBytes();
}

// Subset of src\vm\methodtable.h
[StructLayout(LayoutKind.Explicit)]
internal unsafe struct MethodTableAuxiliaryData
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is the desired direction to expose this, but it's relatively simple.

{
[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