[Bug] Add GC memory pressure tracking for native allocations to avoid memory leaking#607
Open
[Bug] Add GC memory pressure tracking for native allocations to avoid memory leaking#607
Conversation
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
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 wrapper objects (~100 bytes) and was unaware of the ~880+ bytes of unmanaged data per array, so it wouldn't 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. Only the Native path (NativeMemory.Alloc) tracks pressure. External memory paths (np.frombuffer with dispose) are the caller's responsibility. 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)
- Fix incorrect "No GC Pauses" claim in Why Unmanaged Memory section - Add GC Pressure Tracking subsection explaining how NumSharp informs GC - Update transfer ownership example to show AddMemoryPressure best practice
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #501 and implements #605.
GC.AddMemoryPressure()when allocating native memory viaNativeMemory.AllocGC.RemoveMemoryPressure()when freeing itProblem
When creating many NDArrays in a loop, memory would grow to 10GB+ before GC kicked in:
Root Cause
NumSharp allocates array data via
NativeMemory.Alloc(unmanaged) but did not inform the GC about this memory. The GC only saw small managed wrappers (~100 bytes) and was unaware of the ~880+ bytes of unmanaged data per array.Solution
Track memory pressure in
UnmanagedMemoryBlock<T>.Disposer:NativeMemory.Alloc)Test Results
np.array()× 1M (110 doubles)np.array()× 100K (10K doubles)Checklist
bytesCountparameter (default 0)