Garbage Collections

  • DO relieve the pressure on the garbage collector.

    C#/.NET features garbage collection , the process that determines which objects are currently obsolete and removing them to free space in memory. What that means is that in C#, unlike in languages like C++, you don’t have to manually take care of the removal of objects that are no longer useful in order to claim their space in memory. Instead, the garbage collector (GC) handles all of that, so you don’t have to.

    The problem is that there’s no free lunch. The collection process itself causes a performance penalty, so you don’t really want the GC to collect all the time. So how do you avoid that?

    There are many useful techniques to avoid putting too much pressure on the GC. Here, I’ll focus on a single tip: avoid unnecessary allocations. What that means is to avoid things like this:

    List<Product> products = new List<Product>();
    
    products = productRepo.All();
    

    The first line creates an instance of the list that’s completely useless since the very next line returns another instance and assign its reference to the variable. Now imagine the two lines above are inside a loop that executes thousands of times?

    The code above might look like a silly example, but I’ve seen code like this in production — and not just a single time. Don’t focus on the example itself but on the general advice: don’t create objects unless they’re really needed.

    Due to the way the GC works in .NET (it’s a generational GC process), newer objects are more likely to be collected than old ones. That means that the creation of many new, short-lived objects might trigger the GC to run.

Garbage collection can hinder performance — be discerning about creating unnecessary memory allocation work for the GC, for instance by being careful when you choose to create objectsGarbage collection can hinder performance — be discerning about creating unnecessary memory allocation work for the GC, for instance by being careful when you choose to create objects

  • DO NOT use empty destructors.

    The title says it all — don’t add empty destructors to your classes. An entry is added to the Finalize queue for every class that has a destructor. Then, our old friend GC is called to process the queue when the destructor is called. An empty destructor means this is all for nothing.

    Remember, GC execution isn’t cheap in terms of performance, as we’ve already mentioned. Don’t cause work for the GC unnecessarily.

Avoid empty destructors to preserve the GC: don’t use finalizers unless necessary, and use SafeHandle if needed

  • AVOID unnecessary boxing and unboxing.

    Boxing and unboxing are — like garbage collection — expensive processes, performance-wise. So, we want to avoid including them unnecessarily. But what do they do in practice?

    Boxing is like creating a reference type box and putting a value of a value type inside it. In other words, it consists of converting a value type to “object” or to an interface type this value type implements. Unboxing is the opposite—it opens the box and extracts the value type from inside it. Why is that a problem?

    Well, as we’ve mentioned, boxing and unboxing are expensive processes in themselves. Besides that, when you box a value you create another object on the heap, which puts additional pressure on—you’ve guessed it!—the GC.

    So, how to avoid boxing and unboxing?

    In a general way, you can do that by avoiding older APIs in .NET (version 1.0) that predate generics and, as such, have to rely on using the object type. For instance, prefer generic collections such as System.Collections.Generic.List<T>, instead of something like System.Collections.ArrayList.

  • Boxing is efficient for treating small value types as reference types and can simplify your coding process but comes at a performance cost.

  • Avoid using older APIs that rely on object type.

Learn More