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

Allow reference types for pinned GC.AllocateArray() #89293

Merged
merged 16 commits into from
Aug 11, 2023
Merged

Allow reference types for pinned GC.AllocateArray() #89293

merged 16 commits into from
Aug 11, 2023

Conversation

jthorborg
Copy link
Contributor

After an offline discussion, this is the "... or similar" part of the suggested feature set in this issue: #48703

This just relaxes front-end constraints as agreed for pinned array allocations with ref types, which is already possible and utilized internally.

The use case shortly is forming unmanaged references to array indices, where the array has a very long lifetime.

I added some extra coverage of the API, noticing the original constraint wasn't tested in the System.Runtime tests. I'm not sure if further documentation edits are necessary, or if it's fine to just remove the <remarks> section.

It's now possible to allocate pinned and default-initialized arrays that contain references.

Fix #48703
Added a test that pins a reference type array and resolves references to indices with pointer arithmetic, checking that modifications are visible back in the original array.

Test #48703
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Jul 21, 2023
@jthorborg
Copy link
Contributor Author

@dotnet-policy-service agree company="Unity Technologies"

@jkotas
Copy link
Member

jkotas commented Jul 21, 2023

The same fix should be also applied to:
src\coreclr\nativeaot\System.Private.CoreLib\src\System\GC.NativeAot.cs
src\mono\System.Private.CoreLib\src\System\GC.Mono.cs

@ghost
Copy link

ghost commented Jul 21, 2023

Tagging subscribers to this area: @dotnet/gc
See info in area-owners.md if you want to be subscribed.

Issue Details

After an offline discussion, this is the "... or similar" part of the suggested feature set in this issue: #48703

This just relaxes front-end constraints as agreed for pinned array allocations with ref types, which is already possible and utilized internally.

The use case shortly is forming unmanaged references to array indices, where the array has a very long lifetime.

I added some extra coverage of the API, noticing the original constraint wasn't tested in the System.Runtime tests. I'm not sure if further documentation edits are necessary, or if it's fine to just remove the <remarks> section.

Author: jthorborg
Assignees: -
Labels:

area-GC-coreclr, community-contribution

Milestone: -

@jkotas jkotas requested a review from Maoni0 July 21, 2023 11:14
@jthorborg
Copy link
Contributor Author

The same fix should be also applied to: src\coreclr\nativeaot\System.Private.CoreLib\src\System\GC.NativeAot.cs src\mono\System.Private.CoreLib\src\System\GC.Mono.cs

Done, thanks I missed that. Are there tests to update for these as well?

@MichalPetryka
Copy link
Contributor

I think that AllocateUninitializedArray should also be relaxed to allow this since it already accepts reference types.

@jthorborg
Copy link
Contributor Author

I considered that but I worried that garbage bits on reference fields would perhaps not be good news for the GC.

@EgorBo
Copy link
Member

EgorBo commented Jul 21, 2023

Seems like there is a test that expects that exception - AllocateArrayUninitializedPinned_RefType_ThrowsArgumentException

I think that AllocateUninitializedArray should also be relaxed to allow this since it already accepts reference types.

Sounds like a very bad idea to me (unless we want it to work but it will never return garbage)

@MichalPetryka
Copy link
Contributor

(unless we want it to work but it will never return garbage)

That's how it behaves today:

if (!pinned)
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
return new T[length];
}

I considered that but I worried that garbage bits on reference fields would perhaps not be good news for the GC.

Seeing above, I'd say that making the pinned path forward to AllocateArray and return cleared memory makes sense here.

jthorborg and others added 2 commits July 21, 2023 17:16
This was done by deferring reference types to GC.AllocateArray to avoid potential memory issues with the GC + uninitialized memory. The API only promises to avoid initialization if possible, anyway.
Refactored tests to parametrically exercise these new relaxed constraints.
@jthorborg
Copy link
Contributor Author

jthorborg commented Jul 21, 2023

Okay, I've similarly relaxed AllocateUninitializedArray as well by delegating to AllocateArray if it's a reference type. I think it's a reasonable affordance for users, given it's not a promise to return uninitialized memory anyway from this API.

I refactored these tests and combined them to parametric ones to take this into account.

@jkotas
Copy link
Member

jkotas commented Jul 21, 2023

Done, thanks I missed that. Are there tests to update for these as well?

The tests are shared for all runtime flavors. The test updates that you have done should be sufficient.

@jthorborg
Copy link
Contributor Author

Done, thanks I missed that. Are there tests to update for these as well?

The tests are shared for all runtime flavors. The test updates that you have done should be sufficient.

Very nice!

@cshung
Copy link
Member

cshung commented Jul 21, 2023

@Maoni0 - this change relaxes the constraint that user POH allocation cannot contain references. This might impact our plan for #86802 meaning to fix #76929

Relying on internal implementation zeroing refs if necessary.
Mono already piggybacks on the AllocateArray path anyway.
Copy link
Member

@Maoni0 Maoni0 left a comment

Choose a reason for hiding this comment

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

other than my nit comment it LGTM! thanks for doing this work @jthorborg!

Co-authored-by: jthorborg <janus.thorborg@gmail.com>

GC_ALLOC_FLAGS flags = GC_ALLOC_FLAGS.GC_ALLOC_ZEROING_OPTIONAL;
if (pinned)
flags |= GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;

return Unsafe.As<T[]>(AllocateNewArray(RuntimeTypeHandle.ToIntPtr(typeof(T[]).TypeHandle), length, flags));
// Runtime overrides GC_ALLOC_ZEROING_OPTIONAL if the type contains references, so we don't need to worry about that.
return Unsafe.As<T[]>(AllocateNewArray(typeof(T[]).TypeHandle.Value, length, flags));
Copy link
Member

Choose a reason for hiding this comment

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

Why did you change RuntimeTypeHandle.ToIntPtr to RuntimeTypeHandle.Value?

RuntimeTypeHandle.ToIntPtr is JIT intrinsic so it will be optimized to just loading a constant in most cases. RuntimeTypeHandle.Value does not have that optimization. This change may cause a perf regression.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh! I had no idea it was an intrinsic, how is that discoverable?

I changed it to be identical to the similar usage just below: https://github.com/dotnet/runtime/pull/89293/files/d7dfad7341d828be78b09cbe1e49e696d09fac4c#diff-63ddcf9eb1f63c1ddef526934b79b086b1d30883f3293847bf966d047bc7666dR781

Perhaps the should instead revert this line and change the other to use the intrinsic?

Copy link
Member

Choose a reason for hiding this comment

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

Oh! I had no idea it was an intrinsic, how is that discoverable?

JIT intrinsics are marked with [Intrinsic] attributes.

Perhaps the should instead revert this line and change the other to use the intrinsic?

Agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I'll do the change once I get closure on the other discussion

…rray allocation that should be slightly faster.
After a longer discussion, settled on a slightly augmented suggestion that isn't as controversial as the prior one.
@MichalPetryka
Copy link
Contributor

I'd maybe remove the if here

if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
return new T[length];
}

@@ -837,7 +830,7 @@ static T[] AllocateNewUninitializedArray(int length, bool pinned)
throw new OverflowException();

T[]? array = null;
RuntimeImports.RhAllocateNewArray(EETypePtr.EETypePtrOf<T[]>().RawValue, (uint)length, (uint)flags, Unsafe.AsPointer(ref array));
RuntimeImports.RhAllocateNewArray(MethodTable.Of<T[]>(), (uint)length, (uint)flags, Unsafe.AsPointer(ref array));
Copy link
Member

Choose a reason for hiding this comment

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

Could you please also change the type of RhAllocateNewArray argument to MethodTable* to fix the build break

Comment on lines 1065 to 1066
// This isn't a guarantee since CoreCLR reorders fields and ignores the implicit sequential consistency of
// structs, but it will never hurt the test.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// This isn't a guarantee since CoreCLR reorders fields and ignores the implicit sequential consistency of
// structs, but it will never hurt the test.
// The CLR is permitted to reorder fields and ignore the implicit sequential consistency of
// value types if they contain managed references, but it will never hurt the test.

@jthorborg
Copy link
Contributor Author

I'd maybe remove the if here

if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
return new T[length];
}

Wouldn't that leave dead code just below when !DEBUG? Or are you saying remove the whole block instead of just the conditional?

// for debug builds we always want to call AllocateNewArray to detect AllocateNewArray bugs
#if !DEBUG
// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500 // sizeof of managed types
if (length < 2048 / sizeof(T))
#pragma warning restore 8500
{
return new T[length];
}
#endif

@jkotas
Copy link
Member

jkotas commented Aug 10, 2023

Wouldn't that leave dead code just below when !DEBUG? Or are you saying remove the whole block instead of just the conditional?

It looks like that this was fine-tuned based on microbenchmarks. If you delete anything here, you will most likely introduce regressions in micro-benchmarks that exercise this API.

I do not think that this is worth touching. You would have to come up with perf numbers that demonstrate that you are not regressing anything that matters.

* Fixing signature for `RhAllocateNewArray` in NativeAOT to directly use a `MethodTable*`
* Adding explicit structlayout to silence warning for EmbeddedValueType in GCTests, and improved the comment.
@jkotas
Copy link
Member

jkotas commented Aug 11, 2023

The wasm build failure is known infrastructure error according to the build analysis.

@jkotas jkotas merged commit b22aa17 into dotnet:main Aug 11, 2023
@jkotas
Copy link
Member

jkotas commented Aug 11, 2023

Thank you!

@jthorborg
Copy link
Contributor Author

Thank you, especially for the handholding while making green contributions. I'm sure it will be smoother the next time I'll have the pleasure.

However trivial this may seem, it will mean a great deal to us.

@jthorborg jthorborg deleted the poh-allocate-array-allow-reference-types branch August 11, 2023 23:52
@ghost ghost locked as resolved and limited conversation to collaborators Sep 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-GC-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.

7 participants