runtime: OutOfMemory: NativeHeapMemoryBlock should call GC.AddMemoryPressure.

I believe that calling Marshal.AllocHGlobal() without calling GC.AddMemoryPressure (and subsequently GC.RemoveMemoryPressure when releasing the memory) is a mistake.

https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/MemoryBlocks/NativeHeapMemoryBlock.cs

See https://github.com/dotnet/roslyn/issues/24939 for a strong example of why we care about this and why it can lead to OutOfMemory exceptions.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 15 (12 by maintainers)

Most upvoted comments

If it helps, the implementation of the Add/RemoveMemoryPressure API is here: https://github.com/dotnet/runtime/blob/c0ddd1c5d1636de873398c8d9544e02289f95fec/src/coreclr/src/vm/comutilnative.cpp#L1282

Notice that it is not actually built into the GC. It is a simple control loop with a few statics: Cumulative counter of the total memory pressure, and a threshold.

  • When the cumulative counter goes over the threshold, we compute a new bigger threshold and potentially trigger a GC. It is throttled to avoid triggering the GCs too often.
  • When the cumulative counter goes under 1/4 of the threshold, we compute a new smaller threshold.

I agree that the docs for this can be better.

Also, the best practice is to use IDisposable to explicitly dispose expensive unmanaged resources that includes large unmanaged memory blocks. AddMemoryPressure / RemoveMemoryPressure does not help much in this case.

@jkotas, I thought for large allocations you wanted both? That is you want IDisposable to ensure the resource is freed properly without relying on the finalizer, but that won’t inform the GC that you allocated 3GB of native memory while that memory lives (especially if it is long lived). So the pattern would be essentially:

SomeConstructor()
{
    _handle = AllocateMemory(3GB);
    AddMemoryPressure(3GB);
}

~Finalizer()
{
    Dispose(isDisposing: false);
}

Dispose()
{
    Dispose(isDisposing: true);
    GC.SuppressFinalize();
}

Dispose(bool isDisposing)
{
    FreeMemory(_handle);
    RemoveMemoryPressure(3GB);
}

If that isn’t the case, maybe the documentation (https://docs.microsoft.com/en-us/dotnet/api/system.gc.addmemorypressure?view=netframework-4.8) could be improved to better detail when you might want to use the API and provide an example of how to use it properly. CC. @Maoni0

It is not a mistake. 99.99+% code that allocates native memory does not call AddMemoryPressure / RemoveMemoryPressure and it is just fine.

The GC does query the overall state of the process and runs more aggressively when it sees that the memory is getting tight.

Also, the best practice is to use IDisposable to explicitly dispose expensive unmanaged resources that includes large unmanaged memory blocks. AddMemoryPressure / RemoveMemoryPressure does not help much in this case.

AddMemoryPressure / RemoveMemoryPressure helps for cases where the unmanaged resources are disposed via finalizers. It was originally introduced for built-in COM interop that typically does this.