-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Changes from 9 commits
ade6628
246168c
580f969
99b4ad9
6fa4666
6d54f40
5a5da06
ce7f636
7091fb0
a0a1e32
808ceb4
9a8de4c
83e49b7
4303b3c
8dd42ff
e519b2b
9c80cd4
9cd9166
946967e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,12 +13,13 @@ | |
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")] | ||
|
@@ -36,7 +37,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), | ||
|
@@ -66,8 +67,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 | ||
|
@@ -79,8 +95,44 @@ 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); | ||
|
||
// We don't want to expose the method table pointer in the hash code | ||
// Let's use the typeID instead. | ||
uint typeID = RuntimeHelpers.GetTypeID(pMT); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this something we still care about? I assume There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think running the methodtable pointer through |
||
|
||
// To get less colliding and more evenly distributed hash codes, | ||
// we munge the class index with two big prime numbers | ||
int hashCode = (int)(typeID * 711650207 + 2506965631U); | ||
|
||
if (CanCompareBitsOrUseFastGetHashCode(pMT)) | ||
{ | ||
// this is a struct with no refs and no "strange" offsets | ||
HashCode hash = default; | ||
uint size = pMT->GetNumInstanceFieldBytes(); | ||
hash.AddBytes(MemoryMarshal.CreateReadOnlySpan(ref this.GetRawData(), (int)size)); | ||
hashCode ^= hash.ToHashCode(); | ||
} | ||
else | ||
{ | ||
object obj = this; | ||
hashCode ^= RegularGetValueTypeHashCode(pMT, ObjectHandleOnStack.Create(ref obj)); | ||
} | ||
|
||
GC.KeepAlive(this); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be unnecessary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right |
||
return hashCode; | ||
} | ||
|
||
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ValueType_RegularGetValueTypeHashCode")] | ||
[SuppressGCTransition] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The unmanaged slow path manipulates the object reference in COOP mode There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can also throw and call back into managed code. It is not ok to use SuppressGCTransition with that. SuppressGCTransition can be only used for leaf methods. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you would like to make it more managed, you can change the helper to return the strategy to use to compute the hashcode:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got some chanllenge when handling the recursive case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, P/Invoke generator can pin the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would work better if you return field offset from the QCall and compute the byref on the managed side. Nothing to GC protect, etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the recursive value type case, it requires a reference pointing to the field, which is not a top level object. Passing object handle+offset does work, but looks a bit confusing if more levels are involved. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Compound offset like this is not uncommon. It is frequently used e.g. by JITed code. My preference is to have less manually managed code. Less GC mode switches in VM and less manually managed code that runs in a cooperative mode is goodness. |
||
private static unsafe partial int RegularGetValueTypeHashCode(MethodTable* pMT, ObjectHandleOnStack obj); | ||
|
||
public override string? ToString() | ||
{ | ||
|
There was a problem hiding this comment.
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.