Skip to content
Draft
26 changes: 0 additions & 26 deletions src/coreclr/System.Private.CoreLib/src/System/GC.CoreCLR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,33 +124,7 @@ internal enum GC_ALLOC_FLAGS
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern ulong GetGenerationSize(int gen);

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "GCInterface_AddMemoryPressure")]
private static partial void _AddMemoryPressure(ulong bytesAllocated);

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "GCInterface_RemoveMemoryPressure")]
private static partial void _RemoveMemoryPressure(ulong bytesAllocated);

public static void AddMemoryPressure(long bytesAllocated)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytesAllocated);
if (IntPtr.Size == 4)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(bytesAllocated, int.MaxValue);
}

_AddMemoryPressure((ulong)bytesAllocated);
}

public static void RemoveMemoryPressure(long bytesAllocated)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytesAllocated);
if (IntPtr.Size == 4)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(bytesAllocated, int.MaxValue);
}

_RemoveMemoryPressure((ulong)bytesAllocated);
}

// Returns the generation that obj is currently in.
//
Expand Down
271 changes: 97 additions & 174 deletions src/coreclr/vm/comutilnative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1138,14 +1138,66 @@ FORCEINLINE UINT64 GCInterface::InterlockedSub(UINT64 *pMinuend, UINT64 subtrahe
return newMemValue;
}

extern "C" void QCALLTYPE GCInterface_AddMemoryPressure(UINT64 bytesAllocated)
// New helper FCalls for managed AddMemoryPressure/RemoveMemoryPressure implementation
FCIMPL0(INT64, GCInterface::GetCurrentObjSize)
{
QCALL_CONTRACT;
FCALL_CONTRACT;

BEGIN_QCALL;
GCInterface::AddMemoryPressure(bytesAllocated);
END_QCALL;
return (INT64)GCHeapUtilities::GetGCHeap()->GetCurrentObjSize();
}
FCIMPLEND

FCIMPL0(INT64, GCInterface::GetNow)
{
FCALL_CONTRACT;

return (INT64)GCHeapUtilities::GetGCHeap()->GetNow();
}
FCIMPLEND

FCIMPL1(INT64, GCInterface::GetLastGCStartTime, INT32 generation)
{
FCALL_CONTRACT;

return (INT64)GCHeapUtilities::GetGCHeap()->GetLastGCStartTime(generation);
}
FCIMPLEND

FCIMPL1(INT64, GCInterface::GetLastGCDuration, INT32 generation)
{
FCALL_CONTRACT;

return (INT64)GCHeapUtilities::GetGCHeap()->GetLastGCDuration(generation);
}
FCIMPLEND

FCIMPL1(VOID, GCInterface::SendEtwAddMemoryPressureEvent, UINT64 bytesAllocated)
{
FCALL_CONTRACT;

FC_INNER_PROLOG(GCInterface::SendEtwAddMemoryPressureEvent);
FireEtwIncreaseMemoryPressure(bytesAllocated, GetClrInstanceId());
FC_INNER_EPILOG();
}
FCIMPLEND

FCIMPL1(VOID, GCInterface::SendEtwRemoveMemoryPressureEvent, UINT64 bytesAllocated)
{
FCALL_CONTRACT;

FC_INNER_PROLOG(GCInterface::SendEtwRemoveMemoryPressureEvent);
EX_TRY
{
FireEtwDecreaseMemoryPressure(bytesAllocated, GetClrInstanceId());
}
EX_CATCH
{
// Ignore failures
}
EX_END_CATCH
FC_INNER_EPILOG();
}
FCIMPLEND

extern "C" void QCALLTYPE GCInterface_EnumerateConfigurationValues(void* configurationContext, EnumerateConfigurationValuesCallback callback)
{
Expand Down Expand Up @@ -1252,54 +1304,7 @@ uint64_t GCInterface::GetGenerationBudget(int generation)
return GCHeapUtilities::GetGCHeap()->GetGenerationBudget(generation);
}

#ifdef HOST_64BIT
const unsigned MIN_MEMORYPRESSURE_BUDGET = 4 * 1024 * 1024; // 4 MB
#else // HOST_64BIT
const unsigned MIN_MEMORYPRESSURE_BUDGET = 3 * 1024 * 1024; // 3 MB
#endif // HOST_64BIT

const unsigned MAX_MEMORYPRESSURE_RATIO = 10; // 40 MB or 30 MB


// Resets pressure accounting after a gen2 GC has occurred.
void GCInterface::CheckCollectionCount()
{
LIMITED_METHOD_CONTRACT;

IGCHeap * pHeap = GCHeapUtilities::GetGCHeap();

if (m_gc_counts[2] != pHeap->CollectionCount(2))
{
for (int i = 0; i < 3; i++)
{
m_gc_counts[i] = pHeap->CollectionCount(i);
}

m_iteration++;

UINT p = m_iteration % MEM_PRESSURE_COUNT;

m_addPressure[p] = 0; // new pressure will be accumulated here
m_remPressure[p] = 0;
}
}

// AddMemoryPressure implementation
//
// 1. Start budget - MIN_MEMORYPRESSURE_BUDGET
// 2. Focuses more on newly added memory pressure
// 3. Budget adjusted by effectiveness of last 3 triggered GC (add / remove ratio, max 10x)
// 4. Budget maxed with 30% of current managed GC size
// 5. If Gen2 GC is happening naturally, ignore past pressure
//
// Here's a brief description of the ideal algorithm for Add/Remove memory pressure:
// Do a GC when (HeapStart < X * MemPressureGrowth) where
// - HeapStart is GC Heap size after doing the last GC
// - MemPressureGrowth is the net of Add and Remove since the last GC
// - X is proportional to our guess of the ummanaged memory death rate per GC interval,
// and would be calculated based on historic data using standard exponential approximation:
// Xnew = UMDeath/UMTotal * 0.5 + Xprev
//
// Static wrapper methods to call managed AddMemoryPressure/RemoveMemoryPressure
void GCInterface::AddMemoryPressure(UINT64 bytesAllocated)
{
CONTRACTL
Expand All @@ -1310,78 +1315,8 @@ void GCInterface::AddMemoryPressure(UINT64 bytesAllocated)
}
CONTRACTL_END;

CheckCollectionCount();

UINT p = m_iteration % MEM_PRESSURE_COUNT;

UINT64 newMemValue = InterlockedAdd(&m_addPressure[p], bytesAllocated);
Copy link
Member

Choose a reason for hiding this comment

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

Delete GCInterface::InterlockedAdd. It looks unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 8004924. Deleted GCInterface::InterlockedAdd from both comutilnative.h and comutilnative.cpp.


static_assert(MEM_PRESSURE_COUNT == 4, "AddMemoryPressure contains unrolled loops which depend on MEM_PRESSURE_COUNT");

UINT64 add = m_addPressure[0] + m_addPressure[1] + m_addPressure[2] + m_addPressure[3] - m_addPressure[p];
UINT64 rem = m_remPressure[0] + m_remPressure[1] + m_remPressure[2] + m_remPressure[3] - m_remPressure[p];

STRESS_LOG4(LF_GCINFO, LL_INFO10000, "AMP Add: %llu => added=%llu total_added=%llu total_removed=%llu",
bytesAllocated, newMemValue, add, rem);

SendEtwAddMemoryPressureEvent(bytesAllocated);

if (newMemValue >= MIN_MEMORYPRESSURE_BUDGET)
{
UINT64 budget = MIN_MEMORYPRESSURE_BUDGET;

if (m_iteration >= MEM_PRESSURE_COUNT) // wait until we have enough data points
{
// Adjust according to effectiveness of GC
// Scale budget according to past m_addPressure / m_remPressure ratio
if (add >= rem * MAX_MEMORYPRESSURE_RATIO)
{
budget = MIN_MEMORYPRESSURE_BUDGET * MAX_MEMORYPRESSURE_RATIO;
}
else if (add > rem)
{
CONSISTENCY_CHECK(rem != 0);

// Avoid overflow by calculating addPressure / remPressure as fixed point (1 = 1024)
budget = (add * 1024 / rem) * budget / 1024;
}
}

// If still over budget, check current managed heap size
if (newMemValue >= budget)
{
IGCHeap *pGCHeap = GCHeapUtilities::GetGCHeap();
UINT64 heapOver3 = pGCHeap->GetCurrentObjSize() / 3;

if (budget < heapOver3) // Max
{
budget = heapOver3;
}

if (newMemValue >= budget)
{
// last check - if we would exceed 20% of GC "duty cycle", do not trigger GC at this time
if ((size_t)(pGCHeap->GetNow() - pGCHeap->GetLastGCStartTime(2)) > (pGCHeap->GetLastGCDuration(2) * 5))
{
STRESS_LOG6(LF_GCINFO, LL_INFO10000, "AMP Budget: pressure=%llu ? budget=%llu (total_added=%llu, total_removed=%llu, mng_heap=%llu) pos=%d",
newMemValue, budget, add, rem, heapOver3 * 3, m_iteration);

GarbageCollectModeAny(2);

CheckCollectionCount();
}
}
}
}
}

extern "C" void QCALLTYPE GCInterface_RemoveMemoryPressure(UINT64 bytesAllocated)
{
QCALL_CONTRACT;

BEGIN_QCALL;
GCInterface::RemoveMemoryPressure(bytesAllocated);
END_QCALL;
// Call the managed implementation through QCall wrapper
GCInterface_AddMemoryPressureForExternal(bytesAllocated);
}

void GCInterface::RemoveMemoryPressure(UINT64 bytesAllocated)
Expand All @@ -1394,66 +1329,54 @@ void GCInterface::RemoveMemoryPressure(UINT64 bytesAllocated)
}
Copy link
Member

@jkotas jkotas Jan 2, 2026

Choose a reason for hiding this comment

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

@copilot Change the contract of this method THROWS GC_TRIGGERS MODE_ANY. Delete unnecessary switch to pre-emptive mode around the call to this method in LoaderAllocator::Terminate. Change Thread finalizer (ThreadNative::Finalize) to QCall so that it can be call GCInterface::RemoveMemoryPressure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in e94e694. Changed GCInterface::RemoveMemoryPressure contract to THROWS GC_TRIGGERS MODE_ANY, removed GCX_COOP() and GCX_PREEMP() switches, and converted ThreadNative::Finalize to QCall (ThreadNative_Finalize) so it can call the managed RemoveMemoryPressure implementation.

CONTRACTL_END;

CheckCollectionCount();

UINT p = m_iteration % MEM_PRESSURE_COUNT;

SendEtwRemoveMemoryPressureEvent(bytesAllocated);

InterlockedAdd(&m_remPressure[p], bytesAllocated);

STRESS_LOG2(LF_GCINFO, LL_INFO10000, "AMP Remove: %llu => removed=%llu",
bytesAllocated, m_remPressure[p]);
// Call the managed implementation through QCall wrapper
GCInterface_RemoveMemoryPressureForExternal(bytesAllocated);
}

inline void GCInterface::SendEtwAddMemoryPressureEvent(UINT64 bytesAllocated)
// QCall wrappers for managed AddMemoryPressure/RemoveMemoryPressure implementation
extern "C" void QCALLTYPE GCInterface_AddMemoryPressureForExternal(UINT64 bytesAllocated)
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_ANY;
}
CONTRACTL_END;
QCALL_CONTRACT;

FireEtwIncreaseMemoryPressure(bytesAllocated, GetClrInstanceId());
}
BEGIN_QCALL;

// Out-of-line helper to avoid EH prolog/epilog in functions that otherwise don't throw.
NOINLINE void GCInterface::SendEtwRemoveMemoryPressureEvent(UINT64 bytesAllocated)
{
CONTRACTL
{
NOTHROW;
GC_NOTRIGGER;
MODE_ANY;
}
CONTRACTL_END;
GCX_COOP();

EX_TRY
{
FireEtwDecreaseMemoryPressure(bytesAllocated, GetClrInstanceId());
}
EX_CATCH
{
// Ignore failures
}
EX_END_CATCH
// Directly invoke the managed GC.AddMemoryPressure method
MethodTable* pMT = CoreLibBinder::GetClass(CLASS__GC);
MethodDesc* pMD = CoreLibBinder::GetMethod(METHOD__GC__ADD_MEMORY_PRESSURE);

MethodDescCallSite addMemoryPressure(pMD);

ARG_SLOT args[] = {
(ARG_SLOT)bytesAllocated
};

addMemoryPressure.Call(args);

END_QCALL;
}

// Out-of-line helper to avoid EH prolog/epilog in functions that otherwise don't throw.
NOINLINE void GCInterface::GarbageCollectModeAny(int generation)
extern "C" void QCALLTYPE GCInterface_RemoveMemoryPressureForExternal(UINT64 bytesAllocated)
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_ANY;
}
CONTRACTL_END;
QCALL_CONTRACT;

BEGIN_QCALL;

GCX_COOP();
GCHeapUtilities::GetGCHeap()->GarbageCollect(generation, false, collection_non_blocking);

// Directly invoke the managed GC.RemoveMemoryPressure method
MethodDesc* pMD = CoreLibBinder::GetMethod(METHOD__GC__REMOVE_MEMORY_PRESSURE);

MethodDescCallSite removeMemoryPressure(pMD);

ARG_SLOT args[] = {
(ARG_SLOT)bytesAllocated
};

removeMemoryPressure.Call(args);

END_QCALL;
}

//
Expand Down
23 changes: 12 additions & 11 deletions src/coreclr/vm/comutilnative.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,22 @@ class GCInterface {
static FCDECL0(INT64, GetAllocatedBytesForCurrentThread);
static FCDECL0(INT64, GetTotalAllocatedBytesApproximate);

NOINLINE static void SendEtwRemoveMemoryPressureEvent(UINT64 bytesAllocated);
static void SendEtwAddMemoryPressureEvent(UINT64 bytesAllocated);

static void CheckCollectionCount();
static void RemoveMemoryPressure(UINT64 bytesAllocated);
// Helper FCalls for managed AddMemoryPressure/RemoveMemoryPressure implementation
static FCDECL0(INT64, GetCurrentObjSize);
static FCDECL0(INT64, GetNow);
static FCDECL1(INT64, GetLastGCStartTime, INT32 generation);
static FCDECL1(INT64, GetLastGCDuration, INT32 generation);
static FCDECL1(VOID, SendEtwAddMemoryPressureEvent, UINT64 bytesAllocated);
static FCDECL1(VOID, SendEtwRemoveMemoryPressureEvent, UINT64 bytesAllocated);

// Wrapper methods that call managed implementation
static void AddMemoryPressure(UINT64 bytesAllocated);
static void RemoveMemoryPressure(UINT64 bytesAllocated);

static void EnumerateConfigurationValues(void* configurationContext, EnumerateConfigurationValuesCallback callback);
static int RefreshMemoryLimit();
static enable_no_gc_region_callback_status EnableNoGCRegionCallback(NoGCRegionCallbackFinalizerWorkItem* callback, INT64 totalSize);
static uint64_t GetGenerationBudget(int generation);

private:
// Out-of-line helper to avoid EH prolog/epilog in functions that otherwise don't throw.
NOINLINE static void GarbageCollectModeAny(int generation);
};

extern "C" INT64 QCALLTYPE GCInterface_GetTotalAllocatedBytesPrecise();
Expand Down Expand Up @@ -221,9 +222,9 @@ extern "C" int QCALLTYPE GCInterface_StartNoGCRegion(INT64 totalSize, BOOL lohSi

extern "C" int QCALLTYPE GCInterface_EndNoGCRegion();

extern "C" void QCALLTYPE GCInterface_AddMemoryPressure(UINT64 bytesAllocated);
extern "C" void QCALLTYPE GCInterface_AddMemoryPressureForExternal(UINT64 bytesAllocated);

extern "C" void QCALLTYPE GCInterface_RemoveMemoryPressure(UINT64 bytesAllocated);
extern "C" void QCALLTYPE GCInterface_RemoveMemoryPressureForExternal(UINT64 bytesAllocated);

extern "C" void QCALLTYPE GCInterface_ReRegisterForFinalize(QCall::ObjectHandleOnStack pObj);

Expand Down
Loading
Loading