-
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
Conversation
@@ -615,6 +630,28 @@ public TypeHandle GetArrayElementTypeHandle() | |||
public extern uint GetNumInstanceFieldBytes(); | |||
} | |||
|
|||
// Subset of src\vm\methodtable.h | |||
[StructLayout(LayoutKind.Explicit)] | |||
internal unsafe struct MethodTableAuxiliaryData |
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.
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something we still care about? I assume GetType().GetHashCode()
would be slower.
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 think running the methodtable pointer through HashCode
would be good enough. If you do that, the multiplication on the next won't be necessary.
hashCode ^= RegularGetValueTypeHashCode(pMT, ObjectHandleOnStack.Create(ref obj)); | ||
} | ||
|
||
GC.KeepAlive(this); |
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.
Should be unnecessary
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.
Right
} | ||
|
||
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ValueType_RegularGetValueTypeHashCode")] | ||
[SuppressGCTransition] |
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.
The unmanaged slow path manipulates the object reference in COOP mode
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.
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 comment
The 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:
- Call object.GetHashCode for object reference at given offset
- Call double.GetHashCode for field at given offset
- Call float.GetHashCode for field at given offset
- Hash N bytes from given offset (this can also cover the case where
CanCompareBitsOrUseFastGetHashCode
was not computed)
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.
Got some chanllenge when handling the recursive case.
How to pass a reference pointing to the middle of an object to QCall? There's a leaf method GetFieldTypeHandleThrowing
marked as throwing, so it looks unsuitable for FCall since we are moving away from HELPER_METHOD_FRAME.
Or, will it really throw when loading a field handle of already boxed type? Is there any alternate?
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.
Well, P/Invoke generator can pin the ref byte
. Is it reliable/performant for object fields? Can GCPROTECT be omitted then?
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 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 comment
The 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 comment
The 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.
There is also an inconsistency in old code: if the first field is a struct type that overrides struct Struct2
{
public Struct1 field;
}
struct Struct1
{
public string? o;
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(o ?? "");
public override bool Equals(object? obj) => obj is Struct1 other && string.Equals(o, other.o, StringComparison.OrdinalIgnoreCase);
}
Struct1 a1 = new() { o = "ABC" }, b1 = new() { o = "abc" };
Struct2 a2 = new() { field = a1 }, b2 = new() { field = b1 };
Console.WriteLine(a1.Equals(b1)); // true
Console.WriteLine(a1.GetHashCode() == b1.GetHashCode()); // true
Console.WriteLine(a2.Equals(b2)); // true
Console.WriteLine(a2.GetHashCode() == b2.GetHashCode()); // false This can be fixed in follow-up. |
4495372
to
e519b2b
Compare
src/coreclr/vm/comutilnative.cpp
Outdated
CorElementType fieldType = field->GetFieldType(); | ||
if (fieldType == ELEMENT_TYPE_R8) | ||
{ | ||
*fieldOffset = field->GetOffsetUnsafe(); |
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.
*fieldOffset = field->GetOffsetUnsafe(); | |
*fieldOffset += field->GetOffsetUnsafe(); |
I think all these need to be +=
to make the recursive case work.
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 may be a good idea to add a test for the recursive case to src\libraries\system.Runtime\tests\System.Runtime.Tests\System\ValueTypeTests.cs that would catch this mistake.
obj1.o = null; | ||
obj1.value.value = 2; |
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.
obj1.o = null; | |
obj1.value.value = 2; | |
obj2.o = null; | |
obj2.value.value = 2; |
I assume?
@@ -299,6 +299,21 @@ public static void StructContainsPointerCompareTest() | |||
Assert.True(obj1.Equals(obj2)); | |||
Assert.Equal(obj1.GetHashCode(), obj2.GetHashCode()); | |||
} | |||
|
|||
[Fact] | |||
public static void StructContainsPointerNestedCompareTest() |
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.
Because these two values are not the same the fact they aren't equal makes sense. We should also have a test where they are equal to validate the other direction.
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.
Well all the other tests are only testing the equal case. Returning different hash code is just an implementation detail. The test should be reversed and only test equal case instead.
The new test is failing on native AOT:
|
If it looks like a native AOT bug, you can file an issue on it and disable the test against it. |
I don't think it's actually a bug. Different values may have equal hash code, it's not violating the contract. |
Removing the behavior that skips |
I do not think we should be changing the behavior. It can result into arbitrarily large perf regression of hashtable lookups. |
Is there any other change required? I don't think it's necessary to test such implementation detail. |
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.
Thank you!
Also converts HELPER_METHOD_FRAME for slow path to QCall.