25

Does using a lambda expression generate garbage for the GC opposed to the normal foreach loop?

// Lambda version
Foos.ForEach(f=>f.Update(gameTime));

// Normal approach:
foreach (Foo f in Foos)
{
  f.Update(gameTime);
}

The CLR profiler shows that I have 69.9% system.Action< T > and I suspect that being the lamba version of the foreach loop as above. Is that true?

EDIT: I used the Microsoft CLR profiler: http://download.microsoft.com/download/4/4/2/442d67c7-a1c1-4884-9715-803a7b485b82/clr%20profiler.exe or http://msdn.microsoft.com/en-us/library/ff650691.aspx

Napoleon
  • 1,072
  • 5
  • 17
  • 31
  • Could you also post your type definition of foo and foos? – Shiraz Bhaiji Aug 20 '11 at 16:35
  • 1
    Really wish you'd chosen a better example since `ForEach` does absolutely nothing useful. – Aaronaught Aug 20 '11 at 16:37
  • 4
    @Shiraz: Using my amazing powers of deduction, `Foos` is a `List` and `Foo` is any class you like. It doesn't matter for the sake of the question. – StriplingWarrior Aug 20 '11 at 16:38
  • Yeah, I'd say a list since only generic lists have the .ForEach() method (that I'm aware of anyway). – oscilatingcretin Aug 20 '11 at 16:46
  • Definition of Foo... Mmm... It's just a class (or a struct) that holds all kinds of variables like strings and textures and such. Those foo classes however do NOT generate garbage themselves when their Update() is called. They use object pools and such. – Napoleon Aug 20 '11 at 16:47
  • And the foreach example IS useful, it updates every item in the collection and it's not just an example it's the actual code only the name is replaced with Foo and Foos. Foo is the class/struct and Foos it the List< Foo >. – Napoleon Aug 20 '11 at 16:49
  • @SW I tried that, get an error the foo does not contain a definition for Foreach, but it does contain one for ForEach – Shiraz Bhaiji Aug 20 '11 at 16:49

2 Answers2

26

Yes, a lambda will create garbage if the closure captures a variable from the local scope (i.e. gameTime in this context).

For example, the following C# function:

static void TestLambda(List<Foo> Foos, DateTime gameTime)
{
    Foos.ForEach(f => f.Update(gameTime));
}

Will get translated to this:

private static void TestLambda(List<Foo> Foos, DateTime gameTime)
{
    Program.<>c__DisplayClass1 <>c__DisplayClass = new Program.<>c__DisplayClass1();
    <>c__DisplayClass.gameTime = gameTime;
    Foos.ForEach(new Action<Foo>(<>c__DisplayClass.<TestLambda>b__0));
}

Note that there are two instances of new in the resulting code, meaning that there is not only Action objects being allocated (the closures), but also objects to hold the captured variables (escaping variable records).

Gabe
  • 84,912
  • 12
  • 139
  • 238
  • Why would the capture create garbage? I am sure that the generated class is not deep copying the captured variable- it is just getting a reference- and if the variable is a value type (as I suspect it is in this case), there is no GC overhead anyway. – Chris Shain Aug 20 '11 at 16:25
  • @Chris: Assuming `gameTime` is a captured variable, every time `f=>f.Update(gameTime)` is evaluated, a new closure has to be created which points to the enclosing escaping variable record. – Gabe Aug 20 '11 at 16:30
  • 1
    @Chris: You said it yourself: its generating a class that has to be GC'ed eventually. – BrokenGlass Aug 20 '11 at 16:31
  • I guess it depends on your definition of "garbage." Is there more space used in the heap as a result of creating this delegate? Yes. Is there enough that it would explain 69% of your program's memory usage? No. – StriplingWarrior Aug 20 '11 at 16:31
  • 1
    Correction, it is generating a type, and types are never GC'd. There is no reason to assume that it is creating a class instance- the generated method could (and should) be static in this case. – Chris Shain Aug 20 '11 at 16:33
  • 1
    @Gabe: The closure is only created once, not every time the action is evaluated. – StriplingWarrior Aug 20 '11 at 16:33
  • 3
    @Chris: Not entirely true. Because the generated method has to have a reference to `gameTime`, it cannot be static. To test this, try creating a version of the OP's code that uses a static method instead of a delegate. A closure is definitely required here. There is probably one new `Type` created for the delegate, assuming no other delegates in the assembly share the same closure signature, but that's not a GC issue. The GC issue is that when the `Action` is created, there is one additional pointer that there wouldn't have been otherwise. It's there, but it's miniscule. – StriplingWarrior Aug 20 '11 at 16:34
  • 2
    @Chris, Stripling is correct. You are confusing two separate concepts. The closure both requires a hidden type to be generated by the compiler *and* an instance of that type to be created. You are correct, obiously, that the type only has to be *defined* once. But it has to be *instantiated* upon every execution of the enclosing context. – Kirk Woll Aug 20 '11 at 16:44
  • @Stripling, @Chris: Look at my edited answer. I've shown actual decompiled code that has 2 instances of `new` in it, indicating that indeed there is garbage being created on every call to `ForEach`. – Gabe Aug 20 '11 at 16:45
  • 1
    @Gabe, yes, I think Stripling is pointing out that it only creates one instance regardless of how many times the *lambda* is evaluated. However, I presume your intent was describing the evaluation of the lambda *declaration*, which indeed instantiates the object each time. – Kirk Woll Aug 20 '11 at 16:46
  • 1
    OK, I can buy the fact that a capture instance is created for the lambda call, but it is not creating an instance per *iteration*, is it? I am pretty sure it is making an instance per call, which is still not going to generate 70% of the OP's memory usage (unless he is calling it a LOT, and not doing much else). – Chris Shain Aug 20 '11 at 16:51
  • 2
    @Chris: If you have 1000 elements in your list, calling `Foos.ForEach(x => x)` will create 1 delegate, not 1000. However, if your program is mostly doing `.ForEach()` then the delegates will start to be a lot of garbage. – Gabe Aug 20 '11 at 16:54
  • Yes my program does this 60 times per second for EACH lambda expression. I have multiple lambda expressions. Let's say I have a total of 50 of those. 50x60=3000 garbage pointers per second. That's rather bad because I'm not allowed to generate any garbage at all there. 69% could be real because I reduced all other garbage to nearly zero now so any garbage will get a high percentage by the CLR now. – Napoleon Aug 20 '11 at 16:58
  • @user: If you can't produce any garbage, you have to either create the lambdas once (only possible if they don't capture local variables), or just use the `foreach` loop (which shouldn't create garbage so long as collection's `IEnumerator` is a `struct`). – Gabe Aug 20 '11 at 17:05
  • Yes and to make sure it doesn't generate garbage when it is not a struct I must use a for-loop instead of a foreach-loop and don't use any lambda's at all. It's a shame tough as those lambda notations make my code a lot easier to read imo. Thanks for the replies everyone. – Napoleon Aug 20 '11 at 17:17
  • Even if the sample does create garbage, *as far as I know* the CLR is pretty good with destroying objects when they lose scope on the stack. So lambdas will generate garbage if they leave the stack and enter the heap. – Jonathan Dickinson Aug 20 '11 at 20:27
  • @Jon: I'm not sure you understand how the CLR manages memory. If an object is allocated on the stack (and only structs can be, though they aren't always), it stays on the stack until it goes out of scope. If an object is on the heap (all class instances and most struct instances), it is only cleaned up when the GC runs. There is no destruction of heap objects outside of a GC run and no migration between the stack and heap. Since lambdas are never structs, they are never on the stack. – Gabe Aug 21 '11 at 01:24
  • 1
    so can I be sure that `f => f.Update()` without capturing any local variable will create no garbage? I could store this in static property but can I avoid having static property and use `f => f.Update()`. it wont create or allocate temporary object right? – M.kazem Akhgary Oct 01 '17 at 17:13
0

In this case I think that you are using a generic method (ForEach) which will generate a new type (assuming that Foo is a reference type, only one new type will be generated), and the lambda will be compiled as a regular anonymous method. Nothing about that suggests any sort of linear increase in memory usage.

As far as the profiler is concerned, you are not measuring anything about memory or the GC. You are measuring time spent executing the method, and the lambda should not be significantly slower than the 'regular' way.

Chris Shain
  • 50,833
  • 6
  • 93
  • 125
  • 2
    How do you know that the user is profiling CPU rather than memory? – Gabe Aug 20 '11 at 16:24
  • 1
    Because the memory profiler doesn't show % on a function by function basis. It shows memory usage by type. – Chris Shain Aug 20 '11 at 16:27
  • 1
    the OP says % of Action - this does seems like a memory profiler measuring how many instances/bytes this type occupies... and on the MS site it says about CLR profiler that it measures the "allocation profile" - see http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=14727 - from my POV this means it is a memory profiler... – Yahia Aug 20 '11 at 16:30