Skip to content
Draft
28 changes: 0 additions & 28 deletions src/coreclr/System.Private.CoreLib/src/System/GC.CoreCLR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,34 +124,6 @@ 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.
//
public static int GetGeneration(object obj)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,19 @@ private void Initialize()
private static partial void Initialize(ObjectHandleOnStack thread);

/// <summary>Clean up the thread when it goes away.</summary>
~Thread() => InternalFinalize(); // Delegate to the unmanaged portion.
~Thread()
{
InternalFinalize(this);
}

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_Finalize")]
private static partial void InternalFinalize(ObjectHandleOnStack thread);

private static void InternalFinalize(Thread thread)
{
InternalFinalize(ObjectHandleOnStack.Create(ref thread));
}

[MethodImpl(MethodImplOptions.InternalCall)]
private extern void InternalFinalize();

private void ThreadNameChanged(string? value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,140 +513,6 @@ public static int CollectionCount(int generation)
return RuntimeImports.RhGetGcCollectionCount(generation, false);
}

// Support for AddMemoryPressure and RemoveMemoryPressure below.
private const uint PressureCount = 4;
#if TARGET_64BIT
private const uint MinGCMemoryPressureBudget = 4 * 1024 * 1024;
#else
private const uint MinGCMemoryPressureBudget = 3 * 1024 * 1024;
#endif

private const uint MaxGCMemoryPressureRatio = 10;

private static int[] s_gcCounts = new int[] { 0, 0, 0 };

private static long[] s_addPressure = new long[] { 0, 0, 0, 0 };
private static long[] s_removePressure = new long[] { 0, 0, 0, 0 };
private static uint s_iteration;

/// <summary>
/// Resets the pressure accounting after a gen2 GC has occurred.
/// </summary>
private static void CheckCollectionCount()
{
if (s_gcCounts[2] != CollectionCount(2))
{
for (int i = 0; i < 3; i++)
{
s_gcCounts[i] = CollectionCount(i);
}

s_iteration++;

uint p = s_iteration % PressureCount;

s_addPressure[p] = 0;
s_removePressure[p] = 0;
}
}

private static long InterlockedAddMemoryPressure(ref long pAugend, long addend)
{
long oldMemValue;
long newMemValue;

do
{
oldMemValue = pAugend;
newMemValue = oldMemValue + addend;

// check for overflow
if (newMemValue < oldMemValue)
{
newMemValue = long.MaxValue;
}
} while (Interlocked.CompareExchange(ref pAugend, newMemValue, oldMemValue) != oldMemValue);

return newMemValue;
}

/// <summary>
/// New AddMemoryPressure implementation (used by RCW and the CLRServicesImpl class)
/// 1. Less sensitive than the original implementation (start budget 3 MB)
/// 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 is less than 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
/// </summary>
/// <param name="bytesAllocated"></param>
public static void AddMemoryPressure(long bytesAllocated)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytesAllocated);
#if !TARGET_64BIT
ArgumentOutOfRangeException.ThrowIfGreaterThan(bytesAllocated, int.MaxValue);
#endif

CheckCollectionCount();
uint p = s_iteration % PressureCount;
long newMemValue = InterlockedAddMemoryPressure(ref s_addPressure[p], bytesAllocated);

Debug.Assert(PressureCount == 4, "GC.AddMemoryPressure contains unrolled loops which depend on the PressureCount");

if (newMemValue >= MinGCMemoryPressureBudget)
{
long add = s_addPressure[0] + s_addPressure[1] + s_addPressure[2] + s_addPressure[3] - s_addPressure[p];
long rem = s_removePressure[0] + s_removePressure[1] + s_removePressure[2] + s_removePressure[3] - s_removePressure[p];

long budget = MinGCMemoryPressureBudget;

if (s_iteration >= PressureCount) // 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 * MaxGCMemoryPressureRatio)
{
budget = MinGCMemoryPressureBudget * MaxGCMemoryPressureRatio;
}
else if (add > rem)
{
Debug.Assert(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)
{
long heapOver3 = RuntimeImports.RhGetCurrentObjSize() / 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 ((RuntimeImports.RhGetGCNow() - RuntimeImports.RhGetLastGCStartTime(2)) > (RuntimeImports.RhGetLastGCDuration(2) * 5))
{
RuntimeImports.RhCollect(2, InternalGCCollectionMode.NonBlocking);
CheckCollectionCount();
}
}
}
}
}

internal struct GCConfigurationContext
{
internal Dictionary<string, object> Configurations;
Expand Down Expand Up @@ -705,18 +571,6 @@ public static unsafe IReadOnlyDictionary<string, object> GetConfigurationVariabl
return context.Configurations!;
}

public static void RemoveMemoryPressure(long bytesAllocated)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytesAllocated);
#if !TARGET_64BIT
ArgumentOutOfRangeException.ThrowIfGreaterThan(bytesAllocated, int.MaxValue);
#endif

CheckCollectionCount();
uint p = s_iteration % PressureCount;
InterlockedAddMemoryPressure(ref s_removePressure[p], bytesAllocated);
}

public static long GetTotalMemory(bool forceFullCollection)
{
long size = RuntimeImports.RhGetGcTotalMemory();
Expand Down
36 changes: 36 additions & 0 deletions src/coreclr/runtime/GCNativeHelpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
Copy link
Member

Choose a reason for hiding this comment

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

@copilot You have to add this .cpp to makefiles - looks for places where MiscNativeHelpers.cpp and MiscNativeHelpers.h is, these file should be next to them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The files are already added to CMakeLists.txt in both src/coreclr/vm/CMakeLists.txt (line 296) and src/coreclr/nativeaot/Runtime/CMakeLists.txt was initially added but then removed since NativeAOT doesn't need the QCall helpers (it uses RuntimeImport directly). The build succeeds, so the makefiles appear to be correct. Are there other makefiles I should check?

// The .NET Foundation licenses this file to you under the MIT license.

#include "common.h"
#include "GCNativeHelpers.h"
#include "gcheaputilities.h"

extern "C" INT64 QCALLTYPE GC_GetCurrentObjSize()
{
return (INT64)GCHeapUtilities::GetGCHeap()->GetCurrentObjSize();
}

extern "C" INT64 QCALLTYPE GC_GetNow()
{
return (INT64)GCHeapUtilities::GetGCHeap()->GetNow();
}

extern "C" INT64 QCALLTYPE GC_GetLastGCStartTime(INT32 generation)
{
return (INT64)GCHeapUtilities::GetGCHeap()->GetLastGCStartTime(generation);
}

extern "C" INT64 QCALLTYPE GC_GetLastGCDuration(INT32 generation)
{
return (INT64)GCHeapUtilities::GetGCHeap()->GetLastGCDuration(generation);
}

extern "C" void QCALLTYPE GC_SendEtwAddMemoryPressureEvent(UINT64 bytesAllocated)
{
FireEtwIncreaseMemoryPressure(bytesAllocated, GetClrInstanceId());
}

extern "C" void QCALLTYPE GC_SendEtwRemoveMemoryPressureEvent(UINT64 bytesAllocated)
{
FireEtwDecreaseMemoryPressure(bytesAllocated, GetClrInstanceId());
}
14 changes: 14 additions & 0 deletions src/coreclr/runtime/GCNativeHelpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#ifndef __GCNATIVEHELPERS_H__
#define __GCNATIVEHELPERS_H__

extern "C" INT64 QCALLTYPE GC_GetCurrentObjSize();
extern "C" INT64 QCALLTYPE GC_GetNow();
extern "C" INT64 QCALLTYPE GC_GetLastGCStartTime(INT32 generation);
extern "C" INT64 QCALLTYPE GC_GetLastGCDuration(INT32 generation);
extern "C" void QCALLTYPE GC_SendEtwAddMemoryPressureEvent(UINT64 bytesAllocated);
extern "C" void QCALLTYPE GC_SendEtwRemoveMemoryPressureEvent(UINT64 bytesAllocated);

#endif // __GCNATIVEHELPERS_H__
1 change: 1 addition & 0 deletions src/coreclr/vm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ set(VM_SOURCES_WKS
bundle.cpp
${RUNTIME_DIR}/CachedInterfaceDispatch.cpp
CachedInterfaceDispatch_Coreclr.cpp
${RUNTIME_DIR}/GCNativeHelpers.cpp
${RUNTIME_DIR}/MiscNativeHelpers.cpp
cachelinealloc.cpp
callconvbuilder.cpp
Expand Down
21 changes: 13 additions & 8 deletions src/coreclr/vm/comsynchronizable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -595,31 +595,36 @@ void ThreadBaseObject::InitExisting()
}
}

FCIMPL1(void, ThreadNative::Finalize, ThreadBaseObject* pThisUNSAFE)
extern "C" void QCALLTYPE ThreadNative_Finalize(QCall::ObjectHandleOnStack thread)
{
FCALL_CONTRACT;
QCALL_CONTRACT;

THREADBASEREF refThis = (THREADBASEREF)pThisUNSAFE;
Thread* thread = refThis->GetInternal();
BEGIN_QCALL;

GCX_COOP();

THREADBASEREF refThis = (THREADBASEREF)thread.Get();
Thread* pThread = refThis->GetInternal();

// Prevent multiple calls to Finalize
// Objects can be resurrected after being finalized. However, there is no
// race condition here. We always check whether an exposed thread object is
// still attached to the internal Thread object, before proceeding.
if (thread)
if (pThread)
{
refThis->ResetStartHelper();

if (GetThreadNULLOk() != thread)
if (GetThreadNULLOk() != pThread)
{
refThis->ClearInternal();
}

thread->SetThreadState(Thread::TS_Finalized);
pThread->SetThreadState(Thread::TS_Finalized);
Thread::SetCleanupNeededForFinalizedThread();
}

END_QCALL;
}
FCIMPLEND

FCIMPL0(FC_BOOL_RET, ThreadNative::CatchAtSafePoint)
{
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/comsynchronizable.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ class ThreadNative
};

static FCDECL0(INT32, GetOptimalMaxSpinWaitsPerSpinIteration);
static FCDECL1(void, Finalize, ThreadBaseObject* pThis);
static FCDECL0(FC_BOOL_RET, CatchAtSafePoint);
static FCDECL0(FC_BOOL_RET, CurrentThreadIsFinalizerThread);
};

extern "C" void QCALLTYPE ThreadNative_Finalize(QCall::ObjectHandleOnStack thread);
extern "C" void QCALLTYPE ThreadNative_Start(QCall::ThreadHandle thread, int threadStackSize, int priority, BOOL isThreadPool, PCWSTR pThreadName);
extern "C" void QCALLTYPE ThreadNative_SetPriority(QCall::ObjectHandleOnStack thread, INT32 iPriority);
extern "C" void QCALLTYPE ThreadNative_GetCurrentThread(QCall::ObjectHandleOnStack thread);
Expand Down
Loading
Loading