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.
Comments
- Anonymous
August 18, 2008
PingBack from http://housesfunnywallpaper.cn/?p=1316 - Anonymous
August 19, 2008
In addition to your advice regarding the importance of SuppressFinalize, I would also strongly suggest that anyone considering implementing a finalizer first read and understand Joe Duffy's excellent "Never write a finalizer again (well, almost never)" article at http://www.bluebytesoftware.com/blog/NeverWriteAFinalizerAgainWellAlmostNever.aspx. - Anonymous
August 20, 2008
I'm curious, even though you open a file handle to a 1mb file, it does it mean that the actual file contents get read into memory as soon as you open the file handle? I was under the impression that unless you actually used a stream to read the file bytes, that nothing gets read off disk. Is this untrue? If so, I have a LOT of refactoring to do :-)Also, if I open a file handle, AND then open a file stream to read the file bytes, does that mean that since the bytes were loaded into a managed construct (a Stream object) I don't need to add / remove memory pressure on the GC, since the GC is aware of the file size because of the stream object?This example worries me because there are a lot of System.IO.File static methods that read file metadata by first opening a file handle, and then using the handle to get file properties. But looking in Reflector, when the file handle is opened (and closed) it doesnt also add memory pressure to the GC, which could be a big problem.An I misinterpreting how file IO works? - Anonymous
August 20, 2008
Hi JohnThe file handle in my sample code was for illustrative purposes only (I just invented Alloc/FreeHandle for the sake of this sample). The idea was if you have a handle to a large amount of unmanaged memory (in my case a managed pointer to a file opened on the NT heap, not the GC heap). The System.IO classes open the file and read the data onto the GC heap, so adding memory pressure is not necessary.-Chris