Skip to content

Commit 2fc4420

Browse files
committed
fix(memory): add GC memory pressure tracking for unmanaged allocations
Fixes GitHub issue #501 where memory would grow to 10GB+ when creating many NDArrays in a loop without explicit GC.Collect() calls. Root cause: NumSharp allocates data via NativeMemory.Alloc (unmanaged) but did not inform the GC about this memory pressure. The GC only saw small managed objects (~100 bytes each) and didn't know about the ~880+ bytes of unmanaged data per array, so it would not trigger collections frequently enough. Fix: Add GC.AddMemoryPressure() when allocating unmanaged memory and GC.RemoveMemoryPressure() when freeing it. This informs the GC about the true memory footprint so it schedules collections appropriately. Before fix: Creating 1M arrays with 110 doubles each peaked at 10+ GB After fix: Same workload peaks at ~54 MB (stable, proper GC behavior) Changes: - Disposer constructor now takes bytesCount parameter for Native allocs - Call GC.AddMemoryPressure(bytesCount) on allocation - Call GC.RemoveMemoryPressure(bytesCount) on deallocation
1 parent 3cb8eca commit 2fc4420

1 file changed

Lines changed: 14 additions & 3 deletions

File tree

src/NumSharp.Core/Backends/Unmanaged/UnmanagedMemoryBlock`1.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public UnmanagedMemoryBlock(long count)
2929
{
3030
var bytes = BytesCount = count * InfoOf<T>.Size;
3131
var ptr = (IntPtr)NativeMemory.Alloc((nuint)bytes);
32-
_disposer = new Disposer(ptr);
32+
_disposer = new Disposer(ptr, bytes);
3333
Address = (T*)ptr;
3434
Count = count;
3535
}
@@ -984,16 +984,24 @@ private enum AllocationType
984984
private readonly IntPtr Address;
985985
private readonly GCHandle _gcHandle;
986986
private readonly Action _dispose;
987+
private readonly long _bytesCount;
987988

988989

989990
/// <summary>
990991
/// Construct a AllocationType.Native (NativeMemory.Alloc)
991992
/// </summary>
992-
/// <param name="address"></param>
993-
public Disposer(IntPtr address)
993+
/// <param name="address">The address of the allocated memory.</param>
994+
/// <param name="bytesCount">The size in bytes of the allocation (for GC memory pressure tracking).</param>
995+
public Disposer(IntPtr address, long bytesCount)
994996
{
995997
Address = address;
998+
_bytesCount = bytesCount;
996999
_type = AllocationType.Native;
1000+
1001+
// Inform the GC about unmanaged memory allocation so it can
1002+
// schedule collections appropriately (fixes GitHub issue #501)
1003+
if (bytesCount > 0)
1004+
GC.AddMemoryPressure(bytesCount);
9971005
}
9981006

9991007
/// <summary>
@@ -1036,6 +1044,9 @@ private void ReleaseUnmanagedResources()
10361044
{
10371045
case AllocationType.Native:
10381046
NativeMemory.Free((void*)Address);
1047+
// Remove GC memory pressure that was added during allocation
1048+
if (_bytesCount > 0)
1049+
GC.RemoveMemoryPressure(_bytesCount);
10391050
return;
10401051
case AllocationType.Wrap:
10411052
return;

0 commit comments

Comments
 (0)