Criteo R&D Blog
Published in

Criteo R&D Blog

Photo by Clark Van Der Beken on Unsplash

Memory Anti-Patterns in C#

Finalizer and IDisposable usage

Let’s start with a hidden way to referencing an object: implementing a “finalizer”. In C#, you write a method whose name is the name of the class prefixed by ~. The compiler generates an override for the virtual Object.Finalize method. An instance of such a type is treated in a particular way by the Garbage Collector:

  • after it is allocated, a reference is kept in a Finalization internal queue
  • after a collection, if it is no more referenced, this reference is moved into another fReachable internal queue and treated as a root until a dedicated thread calls its finalizer code
  • native resources (i.e. IntPtr fields) must always be cleaned up
  • managed resources (i.e. IDisposable fields) should be disposed when called from Dispose (not from GC)
  • it is only meaningful when called from Dispose (not from GC) to avoid extending its lifetime.
  • it means that the finalizer will never be called. If it were, it would have called Cleanup that would have returned immediately because _disposed is true.

Provide list capacity when possible

It is recommended to provide a capacity when creating a List or a collection instance. The .NET implementation of such classes usually stores the values in an array that need to be resized when new elements are added: it means that:

  1. A new array is allocated
  2. The former values are copied to the new array
  3. The former array is no more referenced

Prefer StringBuilder to +/+= for string concatenation

Creating temporary objects will increase the number of garbage collections and impact performances. Since the string class is immutable, each time you need to get an updated version of a string of characters, the .NET framework ends up creating a new string.

Caching strings and interning

Prefer static cache of read-only objects to recreating them in each call such as in the following example:

Don’t (re)create objects

The first pattern to use is the static classes with static methods to avoid the creation of temporary objects just to call fields-less methods. It is also recommended to pre-compute read-only list instead of re-creating it each time a method gets called like in the following example :

Best practices with LINQ

The LINQ syntax is used extensively all over the source code. However, several patterns are found very often and might impact overall performance.

Prefer IEnumerable<T> to IList<T>

Most of the methods are iterating on sequences represented by IEnumerable<T> either via foreach() or thanks to System.Linq.Enumerable extension methods. IList<T> should be used only when sequence modification is required:

FirstOrDefault and Any are your friends… but might not be needed

First, there is no need to call Any (or even worse ToList().Count > 0) before foreach such as in the following code:

Avoid unnecessary ToList()/ToArray() calls

LINQ queries are supposed to defer their execution until the corresponding sequence is iterated such as with a foreach statement. This is also the case when ToList() or ToArray() are called on such a query:

  1. optimization sake to avoid executing the underlying query several times when it is expensive
  2. removing/adding elements from a sequence
  3. storing the result of a query execution in a class field

Prefer IEnumerable<>.Any to List<>.Exists

When manipulating IEnumerable, it is recommended to use Any instead of ToList().Exists() such as in the following code:

Prefer Any to Count when checking for emptiness

The Any extension methods should be preferred to count computation on IEnumerable because the iteration on the sequence stops as soon as the condition (if any) is fulfilled without allocating any temporary list:

Order in extension methods might matter

When operators are applied to sequences (i.e. IEnumerable), their order might have an impact on the performance of the resulting code. One important rule is to always filter first so the resulting sequences get smaller and smaller to iterate. This is why it is recommended to start a LINQ query by Where filters.

IsEven(1)
IsEven(2)
IsMultipleOf3(2)
IsEven(3)
IsEven(4)
IsMultipleOf3(4)
IsEven(5)
IsEven(6)
IsMultipleOf3(6)
--> 6
--------------------------------
IsEven(1)
IsEven(2)
IsMultipleOf3(2)
IsEven(3)
IsEven(4)
IsMultipleOf3(4)
IsEven(5)
IsEven(6)
IsMultipleOf3(6)
--> 6

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store