.NET Framework 1.1 Performance Guidelines - Garbage Collection

From Guidance Share

Jump to: navigation, search

- J.D. Meier, Srinath Vasireddy, Ashish Babbar, Rico Mariani, and Alex Mackman


Contents

Identify and Analyze Your Application's Allocation Profile

Object size, number of objects, and object lifetime are all factors that impact your application's allocation profile. While allocations are quick, the efficiency of garbage collection depends (among other things) on the generation being collected. Collecting small objects from Gen 0 is the most efficient form of garbage collection because Gen 0 is the smallest and typically fits in the CPU cache. In contrast, frequent collection of objects from Gen 2 is expensive. To identify when allocations occur, and which generations they occur in, observe your application's allocation patterns by using an allocation profiler such as the CLR Profiler.

For more information, see "How To: Use CLR Profiler" at http://msdn.microsoft.com/library/en-us/dnpag/html/scalenethowto13.asp


Avoid Calling GC.Collect

The default GC.Collect method causes a full collection of all generations. Full collections are expensive because literally every live object in the system must be visited to ensure complete collection. Needless to say, exhaustively visiting all live objects could, and usually does, take a significant amount of time. The garbage collector's algorithm is tuned so that it does full collections only when it is likely to be worth the expense of doing so. As a result, do not call GC.Collect directly — let the garbage collector determine when it needs to run.

The garbage collector is designed to be self-tuning and it adjusts its operation to meet the needs of your application based on memory pressure. Programmatically forcing collection can hinder tuning and operation of the garbage collector.

If you have a particular niche scenario where you have to call GC.Collect, consider the following:

  • Call GC.WaitForPendingFinalizers after you call GC.Collect. This ensures that the current thread waits until finalizers for all objects are called.
  • After the finalizers run, there are more dead objects (those that were just finalized) that need to be collected. One more call to GC.Collect collects the remaining dead objects.
System.GC.Collect(); // This gets rid of the dead objects
System.GC.WaitForPendingFinalizers(); // This waits for any finalizers to finish.
System.GC.Collect(); 
// This releases the memory associated with the objects that were just finalized.


Consider Using Weak References with Cached Data

Consider using weak references when you work with cached data, so that cached objects can be resurrected easily if needed or released by garbage collection when there is memory pressure. You should use weak references mostly for objects that are not small in size because the weak referencing itself involves some overhead. They are suitable for medium to large-sized objects stored in a collection.

Consider a scenario where you maintain a custom caching solution for the employee information in your application. By holding onto your object through a WeakReference wrapper, the objects are collected when memory pressure grows during periods of high stress.

If on a subsequent cache lookup, you cannot find the object, re-create it from the information stored in an authoritative persistent source. In this way, you balance the use of cache and persistent medium. The following code demonstrates how to use a weak reference.

void SomeMethod() 
{
  // Create a collection
  ArrayList arr = new ArrayList(5);
  // Create a custom object
  MyObject mo = new MyObject();
  // Create a WeakReference object from the custom object
  WeakReference wk = new WeakReference(mo);
  // Add the WeakReference object to the collection
  arr.Add(wk);
  // Retrieve the weak reference
  WeakReference weakReference = (WeakReference)arr[0];
  MyObject mob = null;
  if( weakReference.IsAlive ){
    mob = (MyOBject)weakReference.Target;
  }
  if(mob==null){
    // Resurrect the object as it has been garbage collected
  }
  //continue because we have the object
}


Prevent the Promotion of Short-Lived Objects

Objects that are allocated and collected before leaving Gen 0 are referred as short-lived objects. The following principles help ensure that your short-lived objects are not promoted:

  • Do not reference short-lived objects from long-lived objects. A common example where this occurs is when you assign a local object to a class level object reference.
     class Customer{
       Order _lastOrder;
       void insertOrder (int ID, int quantity, double amount, int productID){
         Order currentOrder = new Order(ID, quantity, amount, productID);
         currentOrder.Insert();
         this._lastOrder = currentOrder;
       }
     }

Avoid this type of code because it increases the likelihood of the object being promoted beyond Gen 0, which delays the object's resources from being reclaimed. One possible implementation that avoids this issue follows.

     class Customer{
       int _lastOrderID;
       void ProcessOrder (int ID, int quantity, double amount, int productID){
         . . .
         this._lastOrderID = ID;
         . . .
       }
     }

The specific Order class is brought in by ID as needed.

  • Avoid implementing a Finalize method. The garbage collector must promote finalizable objects to older generations to facilitate finalization, which makes them long-lived objects.
  • Avoid having finalizable objects refer to anything. This can cause the referenced object(s) to become long-lived.

References

For more information about garbage collection, see the following resources:


Set Unneeded Member Variables to Null Before Making Long-Running Calls

Before you block on a long-running call, you should explicitly set any unneeded member variables to null before making the call so they can be collected. This is demonstrated in the following code fragment.

class MyClass{
  private string str1;
  private string str2;
 void DoSomeProcessing(…){
   str1= GetResult(…);
   str2= GetOtherResult(…);
 }
 void MakeDBCall(…){
   PrepareForDBCall(str1,str2);
   str1=null;
   str2=null;
   // Make a database (long running) call
 }
}

This advice applies to any objects which are still statically or lexically reachable but are actually not needed:

  • If you no longer need a static variable in your class, or some other class, set it to null.
  • If you can "prune" your state, that is also a good idea. You might be able to eliminate most of a tree before a long-running call, for instance.
  • If there are any objects that could be disposed before the long-running call, set those to null.

Do not set local variables to null (C#) or Nothing (Visual Basic .NET) because the JIT compiler can statically determine that the variable is no longer referenced and there is no need to explicitly set it to null. The following code shows an example using local variables.

void func(…)
{
  String str1;
  str1="abc";
  // Avoid this
  str1=null;
}


Minimize Hidden Allocations

Memory allocation is extremely quick because it involves only a pointer relocation to create space for the new object. However, the memory has to be garbage collected at some point and that can hurt performance, so be aware of apparently simple lines of code that actually result in many allocations. For example, String.Split uses a delimiter to create an array of strings from a source string. In doing so, String.Split allocates a new string object for each string that it has split out, plus one object for the array. As a result, using String.Split in a heavy duty context (such as a sorting routine) can be expensive.

string attendees = "bob,jane,fred,kelly,jim,ann";
// In the following single line the code allocates 6 substrings, 
// outputs the attendees array, and the input separators array
string[] names = attendees.Split( new char[] {','});

Also watch out for allocations that occur inside a loop such as string concatenations using the += operator. Finally, hashing methods and comparison methods are particularly bad places to put allocations because they are often called repeatedly in the context of searching and sorting. For more information about how to handle strings efficiently, see "String Operations" later in this chapter.

Avoid or Minimize Complex Object Graphs

Try to avoid using complex data structures or objects that contain a lot of references to other objects. These can be expensive to allocate and create additional work for the garbage collector. Simpler graphs have superior locality and less code is needed to maintain them. A common mistake is to make the graphs too general.

References


Avoid Preallocating and Chunking Memory

C++ programmers often allocate a large block of memory (using malloc) and then use chunks at a time, to save multiple calls to malloc. This is not advisable for managed code for several reasons:

  • Allocation of managed memory is a quick operation and the garbage collector has been optimized for extremely fast allocations. The main reason for preallocating memory in unmanaged code is to speed up the allocation process. This is not an issue for managed code.
  • If you preallocate memory, you cause more allocations than needed; this can trigger unnecessary garbage collections.
  • The garbage collector is unable to reclaim the memory that you manually recycle.
  • Preallocated memory ages and costs more to recycle when it is ultimately released.
Personal tools