-
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 HELPER_METHOD_FRAME in objectnative.cpp to QCall #97641
Conversation
uint size = pMT->GetNumInstanceFieldBytes(); | ||
return SpanHelpers.SequenceEqual( | ||
ref GetRawData(o1), | ||
ref GetRawData(o2), | ||
size); |
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 really not sure if this is GC safe for object field.
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 presume it needs a nogc region for that
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, this does not work. It needs to stay as FCall or QCall w/ SuppressGCTransition.
// Throws NullReferenceException as expected | ||
MethodTable* pMT = RuntimeHelpers.GetMethodTable(this); | ||
|
||
Type? type = GetTypeIfExists(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.
Would reusing the same variable pessimize JIT? The value may be seen as address exposed.
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.
This method is potentially sharable with GetTypeFromHandleUnsafe, but the unnecessary check for TypeDesc
may hurt performance.
@@ -9,8 +10,14 @@ public partial class Object | |||
{ | |||
// Returns a Type object which represent this object instance. | |||
[Intrinsic] | |||
[MethodImpl(MethodImplOptions.InternalCall)] | |||
public extern Type GetType(); | |||
public unsafe Type GetType() |
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.
First of all, it needs NoInlining on it. It's a jit intrinsic and if it gets inlined it might ruin some opts.
Also, it feels like it's potentially doing more work prior calling runtime helper - are there any benchmarks? Presumably, GetType() is perf critical. I was thinking of optimizing Object.GetType in jit here #88631
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 helper is also doing unnecessary check for null and TypeDesc. With NoInling, the performance is likely to regress a lot.
It's not so clear about the desired approach - expanding in JIT or exposing more fields to managed code? Would GetHashCode also benefit from 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.
The helper is also doing unnecessary check for null and TypeDesc. With NoInling, the performance is likely to regress a lot.
I don't think I follow, I meant NoInlining on top of GetType()
you can check JIT codebase for NI_System_Object_GetType
intrinsics - all those opts won't work if it gets inlined (JIT will have to special case GetTypeFromHandleUnsafe
)
Would GetHashCode also benefit from this?
Object.GetHashCode
can also be optimized on C# side like this #55273, it just needs a jit intrinsic to guarantee GC-safe access for object's header (exterior pointer). or can be expanded in JIT as well.
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's a jit intrinsic and if it gets inlined it might ruin some opts.
JIT intrinsics take precedence over inlining, no? Many JIT intrinsics are inlineable, but we do not have them marked as no inlining.
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.
Also, we use similar managed implementation in native AOT. If this has a problem, the native AOT implementation has the same problem.
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.
JIT intrinsics take precedence over inlining, no? Many JIT intrinsics are inlineable, but we do not have them marked as no inlining.
Nope, JIT is free to inline them. In 95% of cases it's not a problem because most intrinsic-related optimizations happen directly in importer prior inlining. There are only a few which are special and GetObject is one of those
E.g.
case NI_System_Array_Clone:
case NI_System_Collections_Generic_Comparer_get_Default:
case NI_System_Collections_Generic_EqualityComparer_get_Default:
case NI_System_Object_MemberwiseClone:
case NI_System_Threading_Thread_get_CurrentThread:
case NI_System_Object_GetType:
case NI_System_Text_UTF8Encoding_UTF8EncodingSealed_ReadUtf8:
case NI_System_SpanHelpers_SequenceEqual:
case NI_System_Buffer_Memmove:
Some of them are just too big to be inlined anyway, internal calls or it's just not a big deal if jit inlines them.
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.
With AuxiliaryData
being exposed in #97590, all the necessary information for fast path seems to be exposed to managed code. Non-collectible RuntimeType objects are now allocated on FOH so it will always be GC safe.
I'm concerned about the code being fragile and depend more implementation details like the FOH optimization. It's also hard to do optimization for collectible types.
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.
Can we disable the inlining for these in the JIT instead if the JIT does not want them inlined?
Disabled inlining has secondary effect on code quality (forces stackwalkable frame, etc.). It will be hard to avoid perf regressions if the fast path has to be marked as disabled inlining.
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.
Can we disable the inlining for these in the JIT instead if the JIT does not want them inlined?
Yeah, it can it be done
|
||
FC_INNER_RETURN(INT32, GetHashCodeHelper(objRef)); | ||
END_QCALL; |
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.
END_QCALL; | |
END_QCALL; | |
return ret; |
I think it's better to leave |
There is also non-trivial regression for GetHashCode:
Reverting GetHashCode changes too. |
return false; | ||
|
||
// If it's not a value class, don't compare by value | ||
if (!o1.GetType().IsValueType) |
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 going to end up calling reflection? It would be a good idea to check for perf regression.
Note that this FCALL does not have a helper method frame and your change is not deleting, just making it a bit smaller. FCALLs without a helper method frame are not something we are trying to actively eliminate.
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 haven't checked if JIT treats this as intrinsic. Only typeof(T).IsValueType
?
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
Tests are crashing:
|
@@ -139,8 +139,32 @@ public static unsafe void PrepareMethod(RuntimeMethodHandle method, RuntimeTypeH | |||
[MethodImpl(MethodImplOptions.InternalCall)] | |||
internal static extern int TryGetHashCode(object o); | |||
|
|||
public static new unsafe bool Equals(object? o1, object? o2) |
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.
We do not seem to have any test coverage for this method. Could you please add some?
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 how to express the mentioned issue about padding bits.
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 we had any regular test coverage for this API, we would likely see the tests fail intermittently due to the padding issue.
I do not think you need to bother with adding a test that fails deterministically due the padding issue.
// Boolean RuntimeHelpers.Equals(Object, Object) | ||
|
||
Assert.True(RuntimeHelpers.Equals(Guid.Empty, Guid.Empty)); | ||
Assert.False(RuntimeHelpers.Equals(Guid.Empty, Guid.NewGuid())); |
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.
Assert.False(RuntimeHelpers.Equals(Guid.Empty, Guid.NewGuid())); | |
Guid guid = Guid.NewGuid(); | |
Assert.Equals(Guid.Empty == guid, RuntimeHelpers.Equals(Guid.Empty, guid)); |
In case we'd generate an empty GUID.
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.
Guid.NewGuid
won't return empty.
The real thing I'm concerned is that we are taking a dependency of the behavior/contract of NewGuid
.
[Fact] | ||
public static void EqualsTest() | ||
{ | ||
// Boolean RuntimeHelpers.Equals(Object, Object) |
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.
This should also cover comparing non-valuetypes and other paths that are special-cased in the implementation.
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!
Contributes to #95695