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 HELPER_METHOD_FRAME in objectnative.cpp to QCall #97641

Merged
merged 17 commits into from
Feb 26, 2024

Conversation

huoyaoyuan
Copy link
Member

Contributes to #95695

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jan 29, 2024
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Jan 29, 2024
Comment on lines 176 to 180
uint size = pMT->GetNumInstanceFieldBytes();
return SpanHelpers.SequenceEqual(
ref GetRawData(o1),
ref GetRawData(o2),
size);
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 really not sure if this is GC safe for object field.

Copy link
Member

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

Copy link
Member

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.

@huoyaoyuan huoyaoyuan added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jan 29, 2024
// Throws NullReferenceException as expected
MethodTable* pMT = RuntimeHelpers.GetMethodTable(this);

Type? type = GetTypeIfExists(pMT);
Copy link
Member Author

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.

Copy link
Member Author

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()
Copy link
Member

@EgorBo EgorBo Jan 29, 2024

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

Copy link
Member Author

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?

Copy link
Member

@EgorBo EgorBo Jan 29, 2024

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
END_QCALL;
END_QCALL;
return ret;

@huoyaoyuan
Copy link
Member Author

I think it's better to leave GetType as-is and convert the easy ones first. Will do an performance measurement for GetHashCode.

@huoyaoyuan
Copy link
Member Author

There is also non-trivial regression for GetHashCode:

Method Job Toolchain Mean Error StdDev Ratio RatioSD
GetHashCodeFast Job-VYLGUH \FCall\corerun.exe 3.151 ns 0.0345 ns 0.0323 ns 1.99 0.02
GetHashCodeFast Job-APSUVV \ManagedHeader\corerun.exe 10.712 ns 0.0876 ns 0.0819 ns 6.77 0.05
GetHashCodeFast Job-RNEPRL \main\corerun.exe 1.580 ns 0.0031 ns 0.0024 ns 1.00 0.00
GetHashCodeSyncBlock Job-VYLGUH \FCall\corerun.exe 3.822 ns 0.0100 ns 0.0094 ns 1.64 0.01
GetHashCodeSyncBlock Job-APSUVV \ManagedHeader\corerun.exe 12.829 ns 0.0649 ns 0.0575 ns 5.51 0.03
GetHashCodeSyncBlock Job-RNEPRL \main\corerun.exe 2.328 ns 0.0044 ns 0.0041 ns 1.00 0.00

Reverting GetHashCode changes too.

return false;

// If it's not a value class, don't compare by value
if (!o1.GetType().IsValueType)
Copy link
Member

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.

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 haven't checked if JIT treats this as intrinsic. Only typeof(T).IsValueType?

Copy link
Member

Choose a reason for hiding this comment

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

Right

@jkotas
Copy link
Member

jkotas commented Feb 24, 2024

Tests are crashing:

Assert failure(PID 24 [0x00000018], Thread: 24 [0x0018]): !"OBJECTREF being accessed while thread is in preemptive GC mode."
    File: /__w/1/s/src/coreclr/vm/object.cpp:487

@@ -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)
Copy link
Member

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?

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 how to express the mentioned issue about padding bits.

Copy link
Member

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()));
Copy link
Contributor

@MichalPetryka MichalPetryka Feb 25, 2024

Choose a reason for hiding this comment

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

Suggested change
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.

Copy link
Member Author

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)
Copy link
Member

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.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

Thank you!

@jkotas jkotas merged commit 5d14a8d into dotnet:main Feb 26, 2024
149 of 153 checks passed
@huoyaoyuan huoyaoyuan deleted the object-native branch February 27, 2024 04:35
@github-actions github-actions bot locked and limited conversation to collaborators Mar 28, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-VM-coreclr community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants