Search code examples
c#lambdagarbage-collectionclosuresallocation

Why does this lambda closure generate garbage although it is not executed at runtime?


I've noticed that the following code generates heap allocations which trigger the garbage collector at some point and I would like to know why this is the case and how to avoid it:

private Dictionary<Type, Action> actionTable = new Dictionary<Type, Action>();

private void Update(int num)
{
    Action action;
//  if (!actionTable.TryGetValue(typeof(int), out action))
    if (false)
    {
        action = () => Debug.Log(num);
        actionTable.Add(typeof(int), action);
    }
    action?.Invoke();
}

I understand that using a lambda such as () => Debug.Log(num) will generate a small helper class (e.g. <>c__DisplayClass7_0) to hold the local variable. This is why I wanted to test if I could cache this allocation in a dictionary. However, I noticed, that the call to Update leads to allocations even when the lambda code is never reached due to the if-statement. When I comment out the lambda, the allocation disappears from the profiler. I am using the Unity Profiler tool (a performance reporting tool within the Unity game engine) which shows such allocations in bytes per frame while in development/debug mode.

I surmise that the compiler or JIT compiler generates the helper class for the lambda for the scope of the method even though I don't understand why this would be desirable.

Finally, is there any way of caching delegates in this manner without allocating and without forcing the calling code to cache the action in advance? (I do know, that I could also allocate the action once in the client code, but in this example I would strictly like to implement some kind of automatic caching because I do not have complete control over the client).

Disclaimer: This is mostly a theoretical question out of interest. I do realize that most applications will not benefit from micro-optimizations like this.


Solution

  • I surmise that the compiler or JIT compiler generates the helper class for the lambda for the scope of the method even though I don't understand why this would be desirable.

    Consider the case where there's more than one anonymous method with a closure in the same method (a common enough occurrence). Do you want to create a new instance for every single one, or just have them all share a single instance? They went with the latter. There are advantages and disadvantages to either approach.

    Finally, is there any way of caching delegates in this manner without allocating and without forcing the calling code to cache the action in advance?

    Simply move that anonymous method into its own method, so that when that method is called the anonymous method is created unconditionally.

    private void Update(int num)
    {
        Action action = null;
        //  if (!actionTable.TryGetValue(typeof(int), out action))
        if (false)
        {
            Action CreateAction()
            {
                return () => Debug.Log(num);
            }
            action = CreateAction();
            actionTable.Add(typeof(int), action);
        }
        action?.Invoke();
    }
    

    (I didn't check if the allocation happened for a nested method. If it does, make it a non-nested method and pass in the int.)