Skip to content

Commit b22aa17

Browse files
jthorborgjkotas
andauthored
Allow reference types for pinned GC.AllocateArray() (#89293)
* Relaxing constraints of System.GC.AllocateArray<T>. It's now possible to allocate pinned and default-initialized arrays that contain references. Fix #48703 * Adding coverage of GC.AllocateArray with ref types. 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 * Mono/NativeAOT relaxing of System.GC.AllocateArray<T>. * Relaxed GC.AllocateUninitializedArray for ref types as well. 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. * Simplifiy conditionals in AllocateUninitializedArray. Relying on internal implementation zeroing refs if necessary. * Also simplify path for NativeOAT. Mono already piggybacks on the AllocateArray path anyway. * Simplify pinning paths. All conditional paths in GC.Allocate*Array now handle pinning unconditionally out of the main branch. * Don't use `var` if type name doesn't exist explicitly on right-hand side * PR feedback for using terse method tables and JIT intrinsics for GC array allocation that should be slightly faster. * Changed EmbeddedValueType and added comment. After a longer discussion, settled on a slightly augmented suggestion that isn't as controversial as the prior one. * 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. --------- Co-authored-by: Jan Kotas <[email protected]>
1 parent 1c4e4c1 commit b22aa17

File tree

5 files changed

+58
-36
lines changed

5 files changed

+58
-36
lines changed

src/coreclr/System.Private.CoreLib/src/System/GC.CoreCLR.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -741,9 +741,6 @@ internal static void UnregisterMemoryLoadChangeNotification(Action notification)
741741
/// <typeparam name="T">Specifies the type of the array element.</typeparam>
742742
/// <param name="length">Specifies the length of the array.</param>
743743
/// <param name="pinned">Specifies whether the allocated array must be pinned.</param>
744-
/// <remarks>
745-
/// If pinned is set to true, <typeparamref name="T"/> must not be a reference type or a type that contains object references.
746-
/// </remarks>
747744
[MethodImpl(MethodImplOptions.AggressiveInlining)] // forced to ensure no perf drop for small memory buffers (hot path)
748745
public static unsafe T[] AllocateUninitializedArray<T>(int length, bool pinned = false) // T[] rather than T?[] to match `new T[length]` behavior
749746
{
@@ -766,11 +763,8 @@ public static unsafe T[] AllocateUninitializedArray<T>(int length, bool pinned =
766763

767764
#endif
768765
}
769-
else if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
770-
{
771-
ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
772-
}
773766

767+
// Runtime overrides GC_ALLOC_ZEROING_OPTIONAL if the type contains references, so we don't need to worry about that.
774768
GC_ALLOC_FLAGS flags = GC_ALLOC_FLAGS.GC_ALLOC_ZEROING_OPTIONAL;
775769
if (pinned)
776770
flags |= GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;
@@ -784,22 +778,16 @@ public static unsafe T[] AllocateUninitializedArray<T>(int length, bool pinned =
784778
/// <typeparam name="T">Specifies the type of the array element.</typeparam>
785779
/// <param name="length">Specifies the length of the array.</param>
786780
/// <param name="pinned">Specifies whether the allocated array must be pinned.</param>
787-
/// <remarks>
788-
/// If pinned is set to true, <typeparamref name="T"/> must not be a reference type or a type that contains object references.
789-
/// </remarks>
790781
public static T[] AllocateArray<T>(int length, bool pinned = false) // T[] rather than T?[] to match `new T[length]` behavior
791782
{
792783
GC_ALLOC_FLAGS flags = GC_ALLOC_FLAGS.GC_ALLOC_NO_FLAGS;
793784

794785
if (pinned)
795786
{
796-
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
797-
ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
798-
799787
flags = GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;
800788
}
801789

802-
return Unsafe.As<T[]>(AllocateNewArray(typeof(T[]).TypeHandle.Value, length, flags));
790+
return Unsafe.As<T[]>(AllocateNewArray(RuntimeTypeHandle.ToIntPtr(typeof(T[]).TypeHandle), length, flags));
803791
}
804792

805793
[MethodImpl(MethodImplOptions.InternalCall)]

src/coreclr/nativeaot/System.Private.CoreLib/src/System/GC.NativeAot.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -795,9 +795,6 @@ internal static ulong GetSegmentSize()
795795
/// <typeparam name="T">Specifies the type of the array element.</typeparam>
796796
/// <param name="length">Specifies the length of the array.</param>
797797
/// <param name="pinned">Specifies whether the allocated array must be pinned.</param>
798-
/// <remarks>
799-
/// If pinned is set to true, <typeparamref name="T"/> must not be a reference type or a type that contains object references.
800-
/// </remarks>
801798
[MethodImpl(MethodImplOptions.AggressiveInlining)] // forced to ensure no perf drop for small memory buffers (hot path)
802799
public static unsafe T[] AllocateUninitializedArray<T>(int length, bool pinned = false)
803800
{
@@ -819,10 +816,6 @@ public static unsafe T[] AllocateUninitializedArray<T>(int length, bool pinned =
819816
}
820817
#endif
821818
}
822-
else if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
823-
{
824-
ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
825-
}
826819

827820
// kept outside of the small arrays hot path to have inlining without big size growth
828821
return AllocateNewUninitializedArray(length, pinned);
@@ -837,7 +830,7 @@ static T[] AllocateNewUninitializedArray(int length, bool pinned)
837830
throw new OverflowException();
838831

839832
T[]? array = null;
840-
RuntimeImports.RhAllocateNewArray(EETypePtr.EETypePtrOf<T[]>().RawValue, (uint)length, (uint)flags, Unsafe.AsPointer(ref array));
833+
RuntimeImports.RhAllocateNewArray(MethodTable.Of<T[]>(), (uint)length, (uint)flags, Unsafe.AsPointer(ref array));
841834
if (array == null)
842835
throw new OutOfMemoryException();
843836

@@ -851,26 +844,20 @@ static T[] AllocateNewUninitializedArray(int length, bool pinned)
851844
/// <typeparam name="T">Specifies the type of the array element.</typeparam>
852845
/// <param name="length">Specifies the length of the array.</param>
853846
/// <param name="pinned">Specifies whether the allocated array must be pinned.</param>
854-
/// <remarks>
855-
/// If pinned is set to true, <typeparamref name="T"/> must not be a reference type or a type that contains object references.
856-
/// </remarks>
857847
public static unsafe T[] AllocateArray<T>(int length, bool pinned = false)
858848
{
859849
GC_ALLOC_FLAGS flags = GC_ALLOC_FLAGS.GC_ALLOC_NO_FLAGS;
860850

861851
if (pinned)
862852
{
863-
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
864-
ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
865-
866853
flags = GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;
867854
}
868855

869856
if (length < 0)
870857
throw new OverflowException();
871858

872859
T[]? array = null;
873-
RuntimeImports.RhAllocateNewArray(EETypePtr.EETypePtrOf<T[]>().RawValue, (uint)length, (uint)flags, Unsafe.AsPointer(ref array));
860+
RuntimeImports.RhAllocateNewArray(MethodTable.Of<T[]>(), (uint)length, (uint)flags, Unsafe.AsPointer(ref array));
874861
if (array == null)
875862
throw new OutOfMemoryException();
876863

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ internal struct GCHeapHardLimitInfo
249249
internal static extern void RhGetMemoryInfo(ref byte info, GCKind kind);
250250

251251
[LibraryImport(RuntimeLibrary)]
252-
internal static unsafe partial void RhAllocateNewArray(IntPtr pArrayEEType, uint numElements, uint flags, void* pResult);
252+
internal static unsafe partial void RhAllocateNewArray(MethodTable* pArrayEEType, uint numElements, uint flags, void* pResult);
253253

254254
[LibraryImport(RuntimeLibrary)]
255255
internal static unsafe partial void RhAllocateNewObject(IntPtr pEEType, uint flags, void* pResult);

src/libraries/System.Runtime/tests/System/GCTests.cs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,11 +1058,60 @@ private static void AllocateArrayTooLarge()
10581058
Assert.Throws<OutOfMemoryException>(() => GC.AllocateUninitializedArray<double>(int.MaxValue, pinned: true));
10591059
}
10601060

1061-
[Fact]
1062-
private static void AllocateArrayRefType()
1061+
[StructLayout(LayoutKind.Sequential)]
1062+
struct EmbeddedValueType<T>
1063+
{
1064+
// There's a few extra fields here to ensure any reads and writes to Value reasonably are not just offset 0.
1065+
// This is a low-level smoke check that can give a bit of confidence in pointer arithmetic.
1066+
// The CLR is permitted to reorder fields and ignore the sequential consistency of value types if they contain
1067+
// managed references, but it will never hurt the test.
1068+
object _1;
1069+
byte _2;
1070+
public T Value;
1071+
int _3;
1072+
string _4;
1073+
}
1074+
1075+
[Theory]
1076+
[InlineData(false), InlineData(true)]
1077+
private static void AllocateArray_UninitializedOrNot_WithManagedType_DoesNotThrow(bool pinned)
1078+
{
1079+
void TryType<T>()
1080+
{
1081+
GC.AllocateUninitializedArray<T>(100, pinned);
1082+
GC.AllocateArray<T>(100, pinned);
1083+
1084+
GC.AllocateArray<EmbeddedValueType<T>>(100, pinned);
1085+
GC.AllocateUninitializedArray<EmbeddedValueType<T>>(100, pinned);
1086+
}
1087+
1088+
TryType<string>();
1089+
TryType<object>();
1090+
}
1091+
1092+
[Theory]
1093+
[InlineData(false), InlineData(true)]
1094+
private unsafe static void AllocateArrayPinned_ManagedValueType_CanRoundtripThroughPointer(bool uninitialized)
10631095
{
1064-
GC.AllocateUninitializedArray<string>(100);
1065-
Assert.Throws<ArgumentException>(() => GC.AllocateUninitializedArray<string>(100, pinned: true));
1096+
const int length = 100;
1097+
var rng = new Random(0xAF);
1098+
1099+
EmbeddedValueType<string>[] array = uninitialized ? GC.AllocateUninitializedArray<EmbeddedValueType<string>>(length, pinned: true) : GC.AllocateArray<EmbeddedValueType<string>>(length, pinned: true);
1100+
byte* pointer = (byte*)Unsafe.AsPointer(ref array[0]);
1101+
var size = Unsafe.SizeOf<EmbeddedValueType<string>>();
1102+
1103+
for(int i = 0; i < length; ++i)
1104+
{
1105+
int idx = rng.Next(length);
1106+
ref EmbeddedValueType<string> evt = ref Unsafe.AsRef<EmbeddedValueType<string>>(pointer + size * idx);
1107+
1108+
string stringValue = rng.NextSingle().ToString();
1109+
evt.Value = stringValue;
1110+
1111+
Assert.Equal(evt.Value, array[idx].Value);
1112+
}
1113+
1114+
GC.KeepAlive(array);
10661115
}
10671116

10681117
[Fact]

src/mono/System.Private.CoreLib/src/System/GC.Mono.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,6 @@ public static T[] AllocateUninitializedArray<T>(int length, bool pinned = false)
281281
public static T[] AllocateArray<T>(int length, bool pinned = false)
282282
{
283283
if (pinned) {
284-
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
285-
ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
286284
return Unsafe.As<T[]>(AllocPinnedArray(typeof(T[]), length));
287285
}
288286

0 commit comments

Comments
 (0)