.NET Tools
Essential productivity kit for .NET and game developers
What Do These …<>+c… Classes Do in my Memory Snapshots?
There’s nothing we love as much as user feedback. It is a priceless source of insights into how people use tools like dotMemory, what gets them excited – and what gets them confused.
Over the last year we’ve received multiple questions from users seeing classes with ...+<>c...
in their names. They said:
- “…in the object browser we have some instances of an object that ends with +<>c. I can’t find any information what kind of objects this is and I hope that you can help me?”
- “What does
+<>c
mean? I am new to dotMemory.” - “I am looking at some “Survived Objects” and I see a lot of “
MyClass+<>c
“.” - “dotMemory lists a lot of objects that look like “
ClassName+<>c...
” with no explanation.”
Let’s look at what these are!
In dotMemory, we may see the following:
So, what is the mysterious ...+<>c...
class? The answer is quite simple. It’s a helper class the compiler creates to execute a lambda. Actually, we’ve already written about this here in a series of posts on how to fight performance issues in .NET. Let me try to sort out the details one more time, especially considering that there were some changes in the compiler since then.
In this example, we have some StringUtils
class that has a Filter
method used to apply filters on a list of strings:
public static class StringUtils { public static List<string> Filter(List<string> source, Func<string, bool> condition) { var result = new List<string>(); foreach (var item in source) { if (condition(item)) result.Add(item); } return result; } }
We want to use this method to filter out strings that are longer than 3 symbols. We will pass this filter to the condition argument using a lambda. Note that the resulting code (after compilation) will depend on whether the lambda contains a closure (a context that is passed to a lambda, e.g. a local variable declared in a method that calls the lambda).
Let’s take a look at the possible ways to call the Filter
method: via a lambda without closure, via a lambda with closure, and via a method group.
A lambda with no closure
Example
First, let’s use a lambda without a closure. Here we pass the maximum string length (3) as a number, so no additional context is passed to the lambda:
public static class FilterTestNoClosure { public static void FilterLongString() { var list = new List<string> {"abc", "abcd", "abcde"}; var result = StringUtils.Filter(list, s => s.Length > 3); Console.WriteLine(result.Count); } }
Decompiled code
If we decompile this code in dotPeek (our free decompiler), we’ll get the following:
public class FilterTestNoClosure { public void FilterLongString() { List source = new List<string>(); source.Add("abc"); source.Add("abcd"); source.Add("abcde"); // ISSUE: method pointer Console.WriteLine(StringUtils.Filter( source, FilterTestNoClosure.<>c.<>9__0_0 ?? ( FilterTestNoClosure.<>c.<>9__0_0 = new Func<string, bool>( (object) FilterTestNoClosure.<>c.<>9, __methodptr(b__0_0)))).Count); } [CompilerGenerated] [Serializable] private sealed class <>c { public static readonly FilterTestNoClosure.<>c <>9; public static Func<string, bool> <>9__0_0; static <>c() { FilterTestNoClosure.<>c.<>9 = new FilterTestNoClosure.<>c(); } public <>c() { base..ctor(); } internal bool b__0_0(string s) { return s.Length > 3; } } }
As you can see, the compiler has created a separate <>c
class with a static constructor, making the lambda a method in this class (b__0_0
). The method is called via the <>9__0_0
static field. Note that this field is checked against null
and created only once during the first call.
Console.WriteLine(StringUtils.Filter(source, FilterTestNoClosure.<>c.<>9__0_0 ?? (FilterTestNoClosure.<>c.<>9__0_0 = new Func<string, bool>((object) FilterTestNoClosure.<>c.<>9, __methodptr(b__0_0)))).Count);
You may wonder what advantages this approach may have. The answer is that it won’t generate any memory traffic. Even if you call FilterLongString
ten thousand times, the lambda will be created just once.
What about dotMemory?
To ensure, let’s see how the following code looks in dotMemory:
public static void Main(string[] args) { var filterTest = new FilterTestNoClosure(); for (int i = 0; i < 10000; i++) { filterTest.FilterLongString(); } Console.ReadLine(); // the snapshot is taken here }
The Memory Traffic view in dotMemory will look as follows:
As you can see, only one object FilterTestNoClosure+<>c
is created. If we examine this instance using the Key Retention Paths view, we’ll see that it is retained via its static fields. Note that as any static members, these fields will remain in memory for the entire lifetime of the application. But typically, this is not a big deal as such static objects are quite small.
Worth knowing
In the pre-Roslyn-compiler era (Visual Studio 2013 and earlier), a lambda without a closure would compile into a static method inside the same class that calls the lambda. For the sake of performance, a separate class is now created. You can find more details in this Stack Overflow discussion.
The main takeaway
If you’re curious what a ...+<>c...
object makes in your snapshot, check the memory traffic. If the object is created only once and is retained via static references, then it’s definitely a lambda without a closure. Should you worry? Absolutely not. This is normal compiler behavior.
A lambda with a closure
Example
Of course, a more universal approach would be to make the maximum string length a variable:
public class FilterTestClosureInArg { public void FilterLongString(int length) { var list = new List<string> {"abc", "abcd", "abcde"}; var result = StringUtils.Filter(list, s => s.Length > length); Console.WriteLine(result.Count); } }
Decompiled code
But how will this be compiled?
public static class FilterTestClosureInArg { public static void FilterLongString(int length) { FilterTestClosureInArg.<>c__DisplayClass0_0 cDisplayClass00 = new FilterTestClosureInArg.<>c__DisplayClass0_0(); cDisplayClass00.length = length; List source = new List<string>(); source.Add("abc"); source.Add("abcd"); source.Add("abcde"); // ISSUE: method pointer Console.WriteLine(StringUtils.Filter( source, new Func<string, bool>((object) cDisplayClass00, __methodptr(b__0))).Count); } [CompilerGenerated] private sealed class <>c__DisplayClass0_0 { public int length; public <>c__DisplayClass0_0() { base..ctor(); } internal bool b__0(string s) { return s.Length > this.length; } } }
Here the compiler acted differently. It still created a separate class <>c__DisplayClass0__0
, but the class no longer contains static fields. The length argument passed to a lambda is made a public field of the <>c__DisplayClass0__0
class. And what is most important, an instance of <>c__DisplayClass0__0
is created each time the FilterLongString()
is called:
FilterTestClosureInArg.<>c__DisplayClass0_0 cDisplayClass00 = new FilterTestClosureInArg.<>c__DisplayClass0_0();
This results in huge memory traffic if the FilterLongString()
method stays on a hot path (is called frequently). Let’s check this out in dotMemory.
What about dotMemory?
public static void Main(string[] args) { var filterTest = new FilterTestClosureInArg(); for (int i = 0; i < 10000; i++) { filterTest.FilterLongString(3); } Console.ReadLine(); // the snapshot is taken here }
If we look at the Memory Traffic view, we’ll see that there were 10,000 instances of the <>c__DisplayClass0__0
class created and collected.
This is not good from the performance point of view. While object allocation costs almost nothing, object collection is a much more “heavyweight” operation. The only way to avoid this is to rewrite the code so that it doesn’t contain closures. If you want more examples on the topic, take a look at our previous post, Unusual Ways of Boosting Up App Performance. Lambdas and LINQs.
Note that a closure is created in all cases where you pass local context. For example, a local variable passed to a lambda leads to a closure as well:
public class FilterTestClosureInLocalVar { public void FilterLongString() { var length = 3; var list = new List<string> {"abc", "abcd", "abcde"}; var result = StringUtils.Filter(list, s => s.Length > length); Console.WriteLine(result.Count); } }
Worth knowing
LINQ queries have the same implementation under the hood, so the ...+<>c__DisplayClass...
classes may indicate not only lambdas with closures but LINQs as well.
The main takeaway
When inspecting a snapshot, it always worth it to take a look at memory traffic. If you see a lot of allocated/collected objects with ...<>c__DisplayClass...
in their names, you’ll know these are lambdas with closures. When examining these objects, ask yourself two questions:
- Do they really impact performance?
- If yes, can you rewrite the code without a closure?
A method group instead of a lambda
Example
Back in 2006, C# 2.0 introduced the ‘method group conversion’ feature, which simplifies the syntax used to assign a method to a delegate. In our case, this means that we can replace the lambda with a method and then pass this method as an argument.
public class FilterTestMethodGroup { public void FilterLongString() { var list = new List<string> {"abc", "abcd", "abcde"}; var result = StringUtils.Filter(list, Compare); Console.WriteLine(result.Count); } private bool Compare(string s) { return s.Length > 3; } }
Decompiled code
We want to know if this approach will generate memory traffic. A quick look at the decompiled code says that yes, unfortunately, it will.
public class FilterTestMethodGroup { public void FilterLongString() { List source = new List<string>(); source.Add("abc"); source.Add("abcd"); source.Add("abcde"); // ISSUE: method pointer Console.WriteLine(StringUtils.Filter( source, new Func<string, bool>((object) this, __methodptr(Compare))).Count); } private bool Compare(string s) { return s.Length > 3; } public FilterTestMethodGroup() { base..ctor(); } }
The compiler doesn’t create an additional class. Instead, it simply creates a new instance of Func<string, bool>
each time FilterLongString()
is called. Called 10,000 times, it will spawn 10,000 instances.
The main takeaway
When using method groups, exercise the same caution as when you use lambdas with closures. They may generate significant memory traffic if on a hot path.
We hope this post has made things a little clearer. Now, if you encounter classes with ...+<>c...
in their names, first of all, know that these are either lambdas or LINQs and the potential problem here is related to memory traffic because of closures.
Still don’t have dotMemory, but want to check your application for memory traffic? You’re welcome to download and try dotMemory free for 5 days of actual use.
(Note that the easiest way to install dotMemory is to use our Toolbox App. All you need to do is click Install next to dotMemory Portable.)