- Notifications
You must be signed in to change notification settings - Fork5.2k
Convert fast path of ValueType.GetHashCode to managed#97590
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
| // Subset of src\vm\methodtable.h | ||
| [StructLayout(LayoutKind.Explicit)] | ||
| internal unsafestruct 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. | ||
| uinttypeID=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 assumeGetType().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 throughHashCode would be good enough. If you do that, the multiplication on the next won't be necessary.
| hashCode^=RegularGetValueTypeHashCode(pMT,ObjectHandleOnStack.Create(refobj)); | ||
| } | ||
| 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
CanCompareBitsOrUseFastGetHashCodewas not computed)
huoyaoyuanJan 27, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 methodGetFieldTypeHandleThrowing 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 theref 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.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
huoyaoyuan commentedJan 28, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There is also an inconsistency in old code: if the first field is a struct type that overrides structStruct2{publicStruct1field;}structStruct1{publicstring?o;publicoverrideintGetHashCode()=>StringComparer.OrdinalIgnoreCase.GetHashCode(o??"");publicoverrideboolEquals(object?obj)=>objisStruct1other&&string.Equals(o,other.o,StringComparison.OrdinalIgnoreCase);}Struct1a1=new(){o="ABC"},b1=new(){o="abc"};Struct2a2=new(){field=a1},b2=new(){field=b1};Console.WriteLine(a1.Equals(b1));// trueConsole.WriteLine(a1.GetHashCode()==b1.GetHashCode());// trueConsole.WriteLine(a2.Equals(b2));// trueConsole.WriteLine(a2.GetHashCode()==b2.GetHashCode());// false This can be fixed in follow-up. |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
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.
Uh oh!
There was an error while loading.Please reload this page.
| 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?
| } | ||
| [Fact] | ||
| publicstaticvoidStructContainsPointerNestedCompareTest() |
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.
jkotas commentedJan 31, 2024
The new test is failing on native AOT: |
jkotas commentedJan 31, 2024
If it looks like a native AOT bug, you can file an issue on it and disable the test against it. |
huoyaoyuan commentedFeb 20, 2024 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I don't think it's actually a bug. Different values may have equal hash code, it's not violating the contract. |
huoyaoyuan commentedFeb 20, 2024
Removing the behavior that skips |
jkotas commentedFeb 20, 2024
I do not think we should be changing the behavior. It can result into arbitrarily large perf regression of hashtable lookups. |
huoyaoyuan commentedFeb 21, 2024
Is there any other change required? I don't think it's necessary to test such implementation detail. |
jkotas left a comment
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.