Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the allocation being saved here?

Tags:

c#

.net

In "Getting Started with Asynchronous Programming in .NET" by Filip Ekberg, in the "Asynchronous Programming Deep Dive/Working with Attached and Detached Tasks" chapter, he says that by using the service value inside the async anonymous method, it introduces a closure and unnecessary allocation:

enter image description here

Next, he says it's better to pass services as a parameter to the action delegate of the StartNew method which avoids unnecessary allocations by avoiding a closure:

enter image description here

My question is: What is the allocation the author says it's being saved by passing the services as parameter?

To make it easier to investigate, I took a much simpler example and put it in the sharplab.io:

  1. Not passing the parameter:

    using System;
    
    public class C {
        public void M() {
            var i = 1;     
            Func<int> f = () => i + 1;
            f();
        }
    }
    

    which compiles to:

    public class C
    {
        [CompilerGenerated]
        private sealed class <>c__DisplayClass0_0
        {
            public int i;
    
            internal int <M>b__0()
            {
                return i + 1;
            }
        }
    
        public void M()
        {
            <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
            <>c__DisplayClass0_.i = 1;
            new Func<int>(<>c__DisplayClass0_.<M>b__0)();
        }
    }
    

    There are two allocations, one for the generated class and one for the Func<int> delegate.

  2. Passing the parameter:

    using System;
    
    public class C {
        public void M() {
            var i = 1;        
            Func<int, int> f = (x) => x + 1;
            f(i);
        }
    }
    

    which compiles to:

    public class C
    {
        [Serializable]
        [CompilerGenerated]
        private sealed class <>c
        {
            public static readonly <>c <>9 = new <>c();
    
            public static Func<int, int> <>9__0_0;
    
            internal int <M>b__0_0(int x)
            {
                return x + 1;
            }
        }
    
        public void M()
        {
            int arg = 1;
            (<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, int>(<>c.<>9.<M>b__0_0)))(arg);
        }
    }
    

    Here, there are still two allocations, one for the generated class (note there's a static field in the class which creates an instance of the class) and another allocation for the Func<int> delegate.

As far as I can see, in both cases the compiler generates a class and there are two allocations.
The only difference I can see is that in the first case, the generated class has a member:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int i;

This does increase the allocation size, because the generated class occupies more memory than in the 2nd case.

Did I understand it correctly, is this what the author refers to?

like image 973
Don Box Avatar asked Sep 27 '19 07:09

Don Box


1 Answers

The difference is in caching.

In your original code, a new delegate instance is created on every call to M(). In the "clever" version, only a single instance is every created, and stored in the static variable.

So if you only call M() once, the same number of objects will be allocated. If you call M() a million times, you'll have far more objects allocated with the first code than with the second.

This code:

(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, int>(<>c.<>9.<M>b__0_0)))(arg);

... should be read as effectively:

if (cachedDelegate == null)
{
    cachedDelegate = new Func<int, int>(GeneratedClass.CachedInstance.Method);
}
cachedDelegate.Invoke(arg);

The instance of <>c is also cached (referred to above as GeneratedClass.CachedInstance) - only a single instance of that is created. (Although that's less important here, as it only needs to be created when the delegate is created... I'm not sure under what circumstances that compiler optimization is particularly useful.)

like image 55
Jon Skeet Avatar answered Oct 02 '22 14:10

Jon Skeet