| File: Internal\Runtime\FrozenObjectHeapManager.cs | Web Access |
| Project: src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\System.Private.CoreLib.csproj (System.Private.CoreLib) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Runtime; using System.Runtime.CompilerServices; using System.Threading; using Debug = System.Diagnostics.Debug; // Rewrite of src\coreclr\vm\frozenobjectheap.cpp in C# namespace Internal.Runtime { internal unsafe partial class FrozenObjectHeapManager { public static readonly FrozenObjectHeapManager Instance = new FrozenObjectHeapManager(); private readonly Lock m_Crst = new Lock(useTrivialWaits: true); private FrozenObjectSegment m_CurrentSegment; // Default size to reserve for a frozen segment private const nuint FOH_SEGMENT_DEFAULT_SIZE = 4 * 1024 * 1024; // Size to commit on demand in that reserved space private const nuint FOH_COMMIT_SIZE = 64 * 1024; public T? TryAllocateObject<T>() where T : class { MethodTable* pMT = MethodTable.Of<T>(); return Unsafe.As<T>(TryAllocateObject(pMT, pMT->BaseSize)); } private object? TryAllocateObject(MethodTable* type, nuint objectSize) { HalfBakedObject* obj = null; using (m_Crst.EnterScope()) { Debug.Assert(type != null); // _ASSERT(FOH_COMMIT_SIZE >= MIN_OBJECT_SIZE); // Currently we don't support frozen objects with special alignment requirements // TODO: We should also give up on arrays of doubles on 32-bit platforms. // (we currently never allocate them on frozen segments) #if FEATURE_64BIT_ALIGNMENT if (type->RequiresAlign8) { // Align8 objects are not supported yet return null; } #endif // NOTE: objectSize is expected be the full size including header // _ASSERT(objectSize >= MIN_OBJECT_SIZE); if (objectSize > FOH_COMMIT_SIZE) { // The current design doesn't allow objects larger than FOH_COMMIT_SIZE and // since FrozenObjectHeap is just an optimization, let's not fill it with huge objects. return null; } obj = m_CurrentSegment == null ? null : m_CurrentSegment.TryAllocateObject(type, objectSize); // obj is nullptr if the current segment is full or hasn't been allocated yet if (obj == null) { nuint newSegmentSize = FOH_SEGMENT_DEFAULT_SIZE; if (m_CurrentSegment != null) { // Double the reserved size to reduce the number of frozen segments in apps with lots of frozen objects // Use the same size in case if prevSegmentSize*2 operation overflows. nuint prevSegmentSize = m_CurrentSegment.m_Size; newSegmentSize = Math.Max(prevSegmentSize, prevSegmentSize * 2); } m_CurrentSegment = new FrozenObjectSegment(newSegmentSize); // Try again obj = m_CurrentSegment.TryAllocateObject(type, objectSize); // This time it's not expected to be null Debug.Assert(obj != null); } } // end of m_Crst lock IntPtr result = (IntPtr)obj; return Unsafe.As<IntPtr, object>(ref result); } private class FrozenObjectSegment { // Start of the reserved memory, the first object starts at "m_pStart + sizeof(ObjHeader)" (its pMT) private byte* m_pStart; // Pointer to the end of the current segment, ready to be used as a pMT for a new object // meaning that "m_pCurrent - sizeof(ObjHeader)" is the actual start of the new object (header). // // m_pCurrent <= m_SizeCommitted public byte* m_pCurrent; // Memory committed in the current segment // // m_SizeCommitted <= m_pStart + FOH_SIZE_RESERVED public nuint m_SizeCommitted; // Total memory reserved for the current segment public nuint m_Size; private IntPtr m_SegmentHandle; public FrozenObjectSegment(nuint sizeHint) { m_Size = sizeHint; Debug.Assert(m_Size > FOH_COMMIT_SIZE); Debug.Assert(m_Size % FOH_COMMIT_SIZE == 0); void* alloc = ClrVirtualReserve(m_Size); if (alloc == null) { // Try again with the default FOH size if (m_Size > FOH_SEGMENT_DEFAULT_SIZE) { m_Size = FOH_SEGMENT_DEFAULT_SIZE; Debug.Assert(m_Size > FOH_COMMIT_SIZE); Debug.Assert(m_Size % FOH_COMMIT_SIZE == 0); alloc = ClrVirtualReserve(m_Size); } if (alloc == null) { throw new OutOfMemoryException(); } } // Commit a chunk in advance m_pStart = (byte*)ClrVirtualCommit(alloc, FOH_COMMIT_SIZE); if (m_pStart == null) { ClrVirtualFree(alloc, m_Size); throw new OutOfMemoryException(); } m_pCurrent = m_pStart + sizeof(ObjHeader); m_SegmentHandle = RuntimeImports.RhRegisterFrozenSegment(m_pStart, (nuint)m_pCurrent - (nuint)m_pStart, FOH_COMMIT_SIZE, m_Size); if (m_SegmentHandle == IntPtr.Zero) { ClrVirtualFree(alloc, m_Size); throw new OutOfMemoryException(); } m_SizeCommitted = FOH_COMMIT_SIZE; } public HalfBakedObject* TryAllocateObject(MethodTable* type, nuint objectSize) { Debug.Assert((m_pStart != null) && (m_Size > 0)); //_ASSERT(IS_ALIGNED(m_pCurrent, DATA_ALIGNMENT)); //_ASSERT(IS_ALIGNED(objectSize, DATA_ALIGNMENT)); Debug.Assert(objectSize <= FOH_COMMIT_SIZE); Debug.Assert(m_pCurrent >= m_pStart + sizeof(ObjHeader)); nuint spaceUsed = (nuint)(m_pCurrent - m_pStart); nuint spaceLeft = m_Size - spaceUsed; Debug.Assert(spaceUsed >= (nuint)sizeof(ObjHeader)); Debug.Assert(spaceLeft >= (nuint)sizeof(ObjHeader)); // Test if we have a room for the given object (including extra sizeof(ObjHeader) for next object) if (spaceLeft - (nuint)sizeof(ObjHeader) < objectSize) { return null; } // Check if we need to commit a new chunk if (spaceUsed + objectSize + (nuint)sizeof(ObjHeader) > m_SizeCommitted) { // Make sure we don't go out of bounds during this commit Debug.Assert(m_SizeCommitted + FOH_COMMIT_SIZE <= m_Size); if (ClrVirtualCommit(m_pStart + m_SizeCommitted, FOH_COMMIT_SIZE) == null) { throw new OutOfMemoryException(); } m_SizeCommitted += FOH_COMMIT_SIZE; } HalfBakedObject* obj = (HalfBakedObject*)m_pCurrent; obj->SetMethodTable(type); m_pCurrent += objectSize; RuntimeImports.RhUpdateFrozenSegment(m_SegmentHandle, m_pCurrent, m_pStart + m_SizeCommitted); return obj; } } private struct HalfBakedObject { private MethodTable* _methodTable; public void SetMethodTable(MethodTable* methodTable) => _methodTable = methodTable; } } }