Skip to content

Commit 82a8579

Browse files
Implement frozen object heap (#94515)
When allocating a RuntimeType instances, we were creating an object on the pinned object heap, creating a handle to it, and purposefully leaked the handle. The RuntimeTypes live forever. This fragments the pinned object heap. So instead of doing that, port frozen object heap from CoreCLR. This is a line-by-line port. Frozen object heap is a segmented bump memory allocator that interacts with the GC to tell it the boundaries of the segments.
1 parent 38463e2 commit 82a8579

File tree

18 files changed

+405
-48
lines changed

18 files changed

+405
-48
lines changed

src/coreclr/nativeaot/Common/src/Internal/Runtime/CompilerHelpers/StartupCodeHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ private static unsafe void InitializeGlobalTablesForModule(TypeManagerHandle typ
153153

154154
private static unsafe void InitializeModuleFrozenObjectSegment(IntPtr segmentStart, int length)
155155
{
156-
if (RuntimeImports.RhpRegisterFrozenSegment(segmentStart, (IntPtr)length) == IntPtr.Zero)
156+
if (RuntimeImports.RhRegisterFrozenSegment((void*)segmentStart, (nuint)length, (nuint)length, (nuint)length) == IntPtr.Zero)
157157
{
158158
// This should only happen if we ran out of memory.
159159
RuntimeExceptionHelpers.FailFast("Failed to register frozen object segment for the module.");

src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/RuntimeImports.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ internal static class RuntimeImports
1313
{
1414
private const string RuntimeLibrary = "*";
1515

16-
[MethodImpl(MethodImplOptions.InternalCall)]
17-
[RuntimeImport(RuntimeLibrary, "RhpRegisterFrozenSegment")]
18-
internal static extern IntPtr RhpRegisterFrozenSegment(IntPtr pSegmentStart, IntPtr length);
19-
20-
[MethodImpl(MethodImplOptions.InternalCall)]
21-
[RuntimeImport(RuntimeLibrary, "RhpUnregisterFrozenSegment")]
22-
internal static extern void RhpUnregisterFrozenSegment(IntPtr pSegmentHandle);
23-
2416
[RuntimeImport(RuntimeLibrary, "RhpGetModuleSection")]
2517
[MethodImplAttribute(MethodImplOptions.InternalCall)]
2618
private static extern IntPtr RhGetModuleSection(ref TypeManagerHandle module, ReadyToRunSectionType section, out int length);

src/coreclr/nativeaot/Runtime/MiscHelpers.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,12 +356,17 @@ EXTERN_C NATIVEAOT_API int32_t __cdecl RhpGetCurrentThreadStackTrace(void* pOutp
356356
return RhpCalculateStackTraceWorker(pOutputBuffer, outputBufferLength, pAddressInCurrentFrame);
357357
}
358358

359-
COOP_PINVOKE_HELPER(void*, RhpRegisterFrozenSegment, (void* pSegmentStart, size_t length))
359+
EXTERN_C NATIVEAOT_API void* __cdecl RhRegisterFrozenSegment(void * pSection, size_t allocSize, size_t commitSize, size_t reservedSize)
360360
{
361-
return RedhawkGCInterface::RegisterFrozenSegment(pSegmentStart, length);
361+
return RedhawkGCInterface::RegisterFrozenSegment(pSection, allocSize, commitSize, reservedSize);
362362
}
363363

364-
COOP_PINVOKE_HELPER(void, RhpUnregisterFrozenSegment, (void* pSegmentHandle))
364+
EXTERN_C NATIVEAOT_API void __cdecl RhUpdateFrozenSegment(void* pSegmentHandle, uint8_t* allocated, uint8_t* committed)
365+
{
366+
RedhawkGCInterface::UpdateFrozenSegment((GcSegmentHandle)pSegmentHandle, allocated, committed);
367+
}
368+
369+
EXTERN_C NATIVEAOT_API void __cdecl RhUnregisterFrozenSegment(void* pSegmentHandle)
365370
{
366371
RedhawkGCInterface::UnregisterFrozenSegment((GcSegmentHandle)pSegmentHandle);
367372
}

src/coreclr/nativeaot/Runtime/gcrhenv.cpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,23 +368,34 @@ void RedhawkGCInterface::BulkEnumGcObjRef(PTR_RtuObjectRef pRefs, uint32_t cRefs
368368
}
369369

370370
// static
371-
GcSegmentHandle RedhawkGCInterface::RegisterFrozenSegment(void * pSection, size_t SizeSection)
371+
GcSegmentHandle RedhawkGCInterface::RegisterFrozenSegment(void * pSection, size_t allocSize, size_t commitSize, size_t reservedSize)
372372
{
373+
ASSERT(allocSize <= commitSize);
374+
ASSERT(commitSize <= reservedSize);
375+
373376
#ifdef FEATURE_BASICFREEZE
374377
segment_info seginfo;
375378

376379
seginfo.pvMem = pSection;
377380
seginfo.ibFirstObject = sizeof(ObjHeader);
378-
seginfo.ibAllocated = SizeSection;
379-
seginfo.ibCommit = seginfo.ibAllocated;
380-
seginfo.ibReserved = seginfo.ibAllocated;
381+
seginfo.ibAllocated = allocSize;
382+
seginfo.ibCommit = commitSize;
383+
seginfo.ibReserved = reservedSize;
381384

382385
return (GcSegmentHandle)GCHeapUtilities::GetGCHeap()->RegisterFrozenSegment(&seginfo);
383386
#else // FEATURE_BASICFREEZE
384387
return NULL;
385388
#endif // FEATURE_BASICFREEZE
386389
}
387390

391+
// static
392+
void RedhawkGCInterface::UpdateFrozenSegment(GcSegmentHandle seg, uint8_t* allocated, uint8_t* committed)
393+
{
394+
ASSERT(allocated <= committed);
395+
396+
GCHeapUtilities::GetGCHeap()->UpdateFrozenSegment((segment_handle)seg, allocated, committed);
397+
}
398+
388399
// static
389400
void RedhawkGCInterface::UnregisterFrozenSegment(GcSegmentHandle segment)
390401
{

src/coreclr/nativeaot/Runtime/gcrhinterface.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ class RedhawkGCInterface
129129
void * pfnEnumCallback,
130130
void * pvCallbackData);
131131

132-
static GcSegmentHandle RegisterFrozenSegment(void * pSection, size_t SizeSection);
132+
static GcSegmentHandle RegisterFrozenSegment(void * pSection, size_t allocSize, size_t commitSize, size_t reservedSize);
133+
static void UpdateFrozenSegment(GcSegmentHandle seg, uint8_t* allocated, uint8_t* committed);
133134
static void UnregisterFrozenSegment(GcSegmentHandle segment);
134135

135136
#ifdef FEATURE_GC_STRESS
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace Internal.Runtime
7+
{
8+
internal unsafe partial class FrozenObjectHeapManager
9+
{
10+
static void* ClrVirtualReserve(nuint size)
11+
{
12+
// The shim will return null for failure
13+
return (void*)Interop.Sys.MMap(
14+
0,
15+
size,
16+
Interop.Sys.MemoryMappedProtections.PROT_NONE,
17+
Interop.Sys.MemoryMappedFlags.MAP_PRIVATE | Interop.Sys.MemoryMappedFlags.MAP_ANONYMOUS,
18+
-1,
19+
0);
20+
21+
}
22+
23+
static void* ClrVirtualCommit(void* pBase, nuint size)
24+
{
25+
int result = Interop.Sys.MProtect(
26+
(nint)pBase,
27+
size,
28+
Interop.Sys.MemoryMappedProtections.PROT_READ | Interop.Sys.MemoryMappedProtections.PROT_WRITE);
29+
30+
return result == 0 ? pBase : null;
31+
}
32+
33+
static void ClrVirtualFree(void* pBase, nuint size)
34+
{
35+
Debug.Assert(size != 0);
36+
Interop.Sys.MUnmap((nint)pBase, size);
37+
}
38+
}
39+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace Internal.Runtime
7+
{
8+
internal unsafe partial class FrozenObjectHeapManager
9+
{
10+
static void* ClrVirtualReserve(nuint size)
11+
{
12+
return Interop.Kernel32.VirtualAlloc(null, size, Interop.Kernel32.MemOptions.MEM_RESERVE, Interop.Kernel32.PageOptions.PAGE_READWRITE);
13+
}
14+
15+
static void* ClrVirtualCommit(void* pBase, nuint size)
16+
{
17+
return Interop.Kernel32.VirtualAlloc(pBase, size, Interop.Kernel32.MemOptions.MEM_COMMIT, Interop.Kernel32.PageOptions.PAGE_READWRITE);
18+
}
19+
20+
static void ClrVirtualFree(void* pBase, nuint size)
21+
{
22+
// We require the size parameter for Unix implementation sake.
23+
// The Win32 API ignores this parameter because we must pass zero.
24+
// If the caller passed zero, this is going to be broken on Unix
25+
// so let's at least assert that.
26+
Debug.Assert(size != 0);
27+
28+
Interop.Kernel32.VirtualFree(pBase, 0, Interop.Kernel32.MemOptions.MEM_RELEASE);
29+
}
30+
}
31+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime;
6+
using System.Runtime.CompilerServices;
7+
using System.Threading;
8+
9+
using Debug = System.Diagnostics.Debug;
10+
11+
// Rewrite of src\coreclr\vm\frozenobjectheap.cpp in C#
12+
13+
namespace Internal.Runtime
14+
{
15+
internal unsafe partial class FrozenObjectHeapManager
16+
{
17+
public static readonly FrozenObjectHeapManager Instance = new FrozenObjectHeapManager();
18+
19+
private readonly LowLevelLock m_Crst = new LowLevelLock();
20+
private FrozenObjectSegment m_CurrentSegment;
21+
22+
// Default size to reserve for a frozen segment
23+
private const nuint FOH_SEGMENT_DEFAULT_SIZE = 4 * 1024 * 1024;
24+
// Size to commit on demand in that reserved space
25+
private const nuint FOH_COMMIT_SIZE = 64 * 1024;
26+
27+
public T? TryAllocateObject<T>() where T : class
28+
{
29+
MethodTable* pMT = MethodTable.Of<T>();
30+
return Unsafe.As<T?>(TryAllocateObject(pMT, pMT->BaseSize));
31+
}
32+
33+
private object? TryAllocateObject(MethodTable* type, nuint objectSize)
34+
{
35+
HalfBakedObject* obj = null;
36+
37+
m_Crst.Acquire();
38+
39+
try
40+
{
41+
Debug.Assert(type != null);
42+
// _ASSERT(FOH_COMMIT_SIZE >= MIN_OBJECT_SIZE);
43+
44+
// Currently we don't support frozen objects with special alignment requirements
45+
// TODO: We should also give up on arrays of doubles on 32-bit platforms.
46+
// (we currently never allocate them on frozen segments)
47+
#if FEATURE_64BIT_ALIGNMENT
48+
if (type->RequiresAlign8)
49+
{
50+
// Align8 objects are not supported yet
51+
return nullptr;
52+
}
53+
#endif
54+
55+
// NOTE: objectSize is expected be the full size including header
56+
// _ASSERT(objectSize >= MIN_OBJECT_SIZE);
57+
58+
if (objectSize > FOH_COMMIT_SIZE)
59+
{
60+
// The current design doesn't allow objects larger than FOH_COMMIT_SIZE and
61+
// since FrozenObjectHeap is just an optimization, let's not fill it with huge objects.
62+
return null;
63+
}
64+
65+
obj = m_CurrentSegment == null ? null : m_CurrentSegment.TryAllocateObject(type, objectSize);
66+
// obj is nullptr if the current segment is full or hasn't been allocated yet
67+
if (obj == null)
68+
{
69+
nuint newSegmentSize = FOH_SEGMENT_DEFAULT_SIZE;
70+
if (m_CurrentSegment != null)
71+
{
72+
// Double the reserved size to reduce the number of frozen segments in apps with lots of frozen objects
73+
// Use the same size in case if prevSegmentSize*2 operation overflows.
74+
nuint prevSegmentSize = m_CurrentSegment.m_Size;
75+
newSegmentSize = Math.Max(prevSegmentSize, prevSegmentSize * 2);
76+
}
77+
78+
m_CurrentSegment = new FrozenObjectSegment(newSegmentSize);
79+
80+
// Try again
81+
obj = m_CurrentSegment.TryAllocateObject(type, objectSize);
82+
83+
// This time it's not expected to be null
84+
Debug.Assert(obj != null);
85+
}
86+
} // end of m_Crst lock
87+
finally
88+
{
89+
m_Crst.Release();
90+
}
91+
92+
IntPtr result = (IntPtr)obj;
93+
94+
return Unsafe.As<IntPtr, object>(ref result);
95+
}
96+
97+
private class FrozenObjectSegment
98+
{
99+
// Start of the reserved memory, the first object starts at "m_pStart + sizeof(ObjHeader)" (its pMT)
100+
private byte* m_pStart;
101+
102+
// Pointer to the end of the current segment, ready to be used as a pMT for a new object
103+
// meaning that "m_pCurrent - sizeof(ObjHeader)" is the actual start of the new object (header).
104+
//
105+
// m_pCurrent <= m_SizeCommitted
106+
public byte* m_pCurrent;
107+
108+
// Memory committed in the current segment
109+
//
110+
// m_SizeCommitted <= m_pStart + FOH_SIZE_RESERVED
111+
public nuint m_SizeCommitted;
112+
113+
// Total memory reserved for the current segment
114+
public nuint m_Size;
115+
116+
private IntPtr m_SegmentHandle;
117+
118+
public FrozenObjectSegment(nuint sizeHint)
119+
{
120+
m_Size = sizeHint;
121+
122+
Debug.Assert(m_Size > FOH_COMMIT_SIZE);
123+
Debug.Assert(m_Size % FOH_COMMIT_SIZE == 0);
124+
125+
void* alloc = ClrVirtualReserve(m_Size);
126+
if (alloc == null)
127+
{
128+
// Try again with the default FOH size
129+
if (m_Size > FOH_SEGMENT_DEFAULT_SIZE)
130+
{
131+
m_Size = FOH_SEGMENT_DEFAULT_SIZE;
132+
Debug.Assert(m_Size > FOH_COMMIT_SIZE);
133+
Debug.Assert(m_Size % FOH_COMMIT_SIZE == 0);
134+
alloc = ClrVirtualReserve(m_Size);
135+
}
136+
137+
if (alloc == null)
138+
{
139+
throw new OutOfMemoryException();
140+
}
141+
}
142+
143+
// Commit a chunk in advance
144+
m_pStart = (byte*)ClrVirtualCommit(alloc, FOH_COMMIT_SIZE);
145+
if (m_pStart == null)
146+
{
147+
ClrVirtualFree(alloc, m_Size);
148+
throw new OutOfMemoryException();
149+
}
150+
151+
m_pCurrent = m_pStart + sizeof(ObjHeader);
152+
153+
m_SegmentHandle = RuntimeImports.RhRegisterFrozenSegment(m_pStart, (nuint)m_pCurrent - (nuint)m_pStart, FOH_COMMIT_SIZE, m_Size);
154+
if (m_SegmentHandle == IntPtr.Zero)
155+
{
156+
ClrVirtualFree(alloc, m_Size);
157+
throw new OutOfMemoryException();
158+
}
159+
160+
m_SizeCommitted = FOH_COMMIT_SIZE;
161+
}
162+
163+
public HalfBakedObject* TryAllocateObject(MethodTable* type, nuint objectSize)
164+
{
165+
Debug.Assert((m_pStart != null) && (m_Size > 0));
166+
//_ASSERT(IS_ALIGNED(m_pCurrent, DATA_ALIGNMENT));
167+
//_ASSERT(IS_ALIGNED(objectSize, DATA_ALIGNMENT));
168+
Debug.Assert(objectSize <= FOH_COMMIT_SIZE);
169+
Debug.Assert(m_pCurrent >= m_pStart + sizeof(ObjHeader));
170+
171+
nuint spaceUsed = (nuint)(m_pCurrent - m_pStart);
172+
nuint spaceLeft = m_Size - spaceUsed;
173+
174+
Debug.Assert(spaceUsed >= (nuint)sizeof(ObjHeader));
175+
Debug.Assert(spaceLeft >= (nuint)sizeof(ObjHeader));
176+
177+
// Test if we have a room for the given object (including extra sizeof(ObjHeader) for next object)
178+
if (spaceLeft - (nuint)sizeof(ObjHeader) < objectSize)
179+
{
180+
return null;
181+
}
182+
183+
// Check if we need to commit a new chunk
184+
if (spaceUsed + objectSize + (nuint)sizeof(ObjHeader) > m_SizeCommitted)
185+
{
186+
// Make sure we don't go out of bounds during this commit
187+
Debug.Assert(m_SizeCommitted + FOH_COMMIT_SIZE <= m_Size);
188+
189+
if (ClrVirtualCommit(m_pStart + m_SizeCommitted, FOH_COMMIT_SIZE) == null)
190+
{
191+
throw new OutOfMemoryException();
192+
}
193+
m_SizeCommitted += FOH_COMMIT_SIZE;
194+
}
195+
196+
HalfBakedObject* obj = (HalfBakedObject*)m_pCurrent;
197+
obj->SetMethodTable(type);
198+
199+
m_pCurrent += objectSize;
200+
201+
RuntimeImports.RhUpdateFrozenSegment(m_SegmentHandle, m_pCurrent, m_pStart + m_SizeCommitted);
202+
203+
return obj;
204+
}
205+
}
206+
207+
private struct HalfBakedObject
208+
{
209+
private MethodTable* _methodTable;
210+
public void SetMethodTable(MethodTable* methodTable) => _methodTable = methodTable;
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)