Using the System.GC APIs to Improve Performance

Chris Lyon here. You may remember me from such blogs as How I Learned to Stop Worrying and Love the GC. I’m writing this blog entry to point out some of the System.GC APIs that can be used to help improve the performance of your managed application.

 

AddMemoryPressure and RemoveMemoryPressure

The Garbage Collector (GC) handles managed memory and only managed memory. That means it’s up to you to manage any other resources your application holds onto (files, network or database connections, etc). AddMemoryPressure and RemoveMemoryPressure, added in .NET 2.0, are used to give a hint to the GC about the size of these unmanaged resources.

Consider the following contrived class which holds a handle to an open file (AllocHandle and FreeHandle are made-up functions for illustration purposes):

class FileHandleHolder

{

    protected IntPtr _fileHandle;

    public FileHandleHolder(string fileName)

    {

       _fileHandle = AllocHandle(fileName);

    }

 

    ~FileHandleHolder()

    {

FreeHandle(_fileHandle);
}

}

 

The handle itself is an IntPtr, which takes up much less memory than the file itself. As far as the GC is concerned, an instance of FileHandleHolder doesn’t exert much memory pressure, so the GC tunes itself accordingly. If however, fileHandle pointed to a 1 GB file, the GC still thinks there isn’t much memory pressure caused by this class, since the file is not on the managed heap. So how do we trick the GC into taking into account our 1 GB file? Let’s make two small changes to our FileHandleHolder class:

class FileHandleHolder

{

    protected IntPtr _fileHandle;

    public FileHandleHolder(string filename, long fileSize)

    {

       _fileHandle = AllocHandle(fileName);

GC.AddMemoryPressure(fileSize);

    }

 

    ~FileHandleHolder()

    {

FreeHandle(_fileHandle);

GC.RemoveMemoryPressure(fileSize);

}

}

 

Our first change is to the constructor. We can pass the size of the actual file to GC.AddMemoryPressure. This will tell the GC that this application has an additional 1 GB of memory pressure to it, and the GC will adjust its tuning accordingly. The second change is to the finalizer. We call GC.RemoveMemoryPressure to tell the GC there is 1 GB less pressure.

A word of warning, make sure you balance your Add/Removes, lest you cause the GC to start collecting more aggressively and hurting your performance.

Collect

We’ve all heard the advice: don’t call GC.Collect. It interferes with the GC’s own tuning and unless you really know what you’re doing, you can hurt your application’s overall performance. In .NET 3.5, we added an overload to GC.Collect that takes a GCCollectionMode enum value. In places where you think you could benefit from a call to GC.Collect (Perf Guru Rico Mariani has some advice about that here: https://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspx), you can call GC.Collect(2, GCCollectionMode.Optimized), which tells the GC to use its best judgment about whether to actually do a GC. Based on the GC’s tuning to that point, it will decide if the GC heap will benefit from a collection or not.

 

SuppressFinalize

Finalizers are bad for performance. After being marked no longer reachable from user code following a collection, your finalizable object stays in memory until its finalizer is run, and only after the next collection is the memory actually reclaimed. And not just your finalizable object, the entire object graph your object references.

To avoid the need for finalizers, we recommend implementing the Dispose Pattern (https://msdn.microsoft.com/en-us/library/fs2xkftw.aspx). This is where GC.SuppressFinalize comes in. Let’s consider our FileHandleHolder class again, but this time have it use the Dispose Pattern:

class FileHandleHolder : IDisposable

{

    protected IntPtr _fileHandle;

    public FileHandleHolder(string filename, long fileSize)

    {

       _fileHandle = AllocHandle(fileName);

GC.AddMemoryPressure(fileSize);

    }

 

    ~FileHandleHolder()

    {

Dispose(false);
}

    public void Dispose()

    {

       Dispose(true);

       GC.SuppressFinalize(this);

    }

    protected virtual void Dispose(bool disposing)

    {

FreeHandle(_fileHandle);

GC.RemoveMemoryPressure(fileSize);

    }

}

Using the Dispose Pattern, the GC gets the hint that the memory pressure is removed sooner than waiting for it to be collected then finalized, and the GC can resume its tuning.

 

Stay tuned for my next blog post that will describe some of the new GC APIs added in .NET 3.5 SP1.

This post was authored by Chris Lyon of the CLR performance team at Microsoft Corporation.