0

I asked this question and found out that an instance of a closure will be created

at the outer most scope it "needs" to capture the free variables

Here's the example code again for reference:

    /// <summary>
    /// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
    /// </summary>
    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;

        if (!_cache.TryGetValue(name, out TOptions options))
        {
            // Store the options in our instance cache. Avoid closure on fast path by storing state into scoped locals.
            IOptionsFactory<TOptions> localFactory = _factory;
            string localName = name;
            options = _cache.GetOrAdd(name, () => localFactory.Create(localName));
        }

        return options;
    }

If _factory and name were used instead of creating local variables, why would the closure be instantiated before the if statement? Why can't it create a closure using _factory and name only if it enters the if statement?

David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • Just out of curiosity, what is the type of `_cache`? It seems a `ConcurrentDictionary`, but the `GetOrAdd` doesn't compile on my pc... – xanatos Jan 02 '21 at 10:24
  • I don't have closures [here](https://sharplab.io/#v2:EYLgtghgzgLgpgJwD4AEAMACFBGAdAYQHsAbYuAYxgEtCA7KAu8gVwQTlpgG4BYAKH4oAzFgBMGAPIAHanSgZ+Ab34ZVWETgBsWbJgAicAGYRmxGADkIYOBgC8GAEQAxQoQe8+AX36CRVTojG5DYAktKy9E4QlIQIAJ4APAAqAHxKKmpJGPjsEPAAFDiYsAgAlB7eAnzCYhgAsnH4xNBQyeE09Gl8ynxq6tlMrOycelSUHRDxCUUANBhJ7XIpGAD65NEAFjb2tHAA7gO0LGwcMKPjdJOJs/OLnfnlPr1qNWEyHVBRMVML70urQRgsTidgwtFMxA8GVUNQAblQEDBmBBiLc/vQMABxOAwQq6MFWOClaEYHp9Pq0QmgynWDAAfjpknRDAMxlMFkJUOe5IwVEMGHyAEI1ps4LgkvFsTAAGoo5hwfI0uBzQjMGBoiLyQjM0rE7nksk88nazWgkXkLa4KUSBAAQQAJvbFYS5gAPOzLFaA4EEXIFV26jxGjCVEl9FAAdgwJo+QbUlUqQA=) – xanatos Jan 02 '21 at 10:28
  • Can you elaborate on the differences with your code? – xanatos Jan 02 '21 at 10:28
  • 1
    @xanatos https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Options/src/OptionsManager.cs#L18. Also, notice that this post's code comes from .NET Core source code, look at the previous question – Camilo Terevinto Jan 02 '21 at 10:38
  • https://sharplab.io/#v2:D4AQDABCCMB0DCB7ANsgpgYwC4EtEDsBnBAjAVwCcK18sBuAWAChmQBmCHWtCgMwEMMaCAEkA8gAdcBQgDFBWRBQCeAHgAqAPmYBvZhAMR1EeNX5Y0AChiRCWCgEpGTAL7NWHEACYIAWWXwyPyEhBqS0kTaTHpMhlAcSPjkVDRYACI42Hj4/CqqNgA0RuHZhJoQAPoYggAWwgC8EPhoAO4mpJTUtBlZBLlqhcVSpZqWTvqG7KIlMvLYSmrqM5GVAvMqEI34ZKjOEwZTS8MyEADiaFjW0JA5ALZoDvsQMXFxOLwQlgCEVbVosOoVOcsAA1fjIMhWO5oIqIMhYIYRQgQRDHIgOR6xV4GF7Y142Jr8e4+LZEtDOPGvVFIzaVaoYOqwYFiCgAQQAJuzLNCigAPTblCprRQqBBmCzcsleDEUvFuLHYp5xAD0yvat3utAgWBqOGR7yVhnenx+9MZgOUwLBEKhZNh8MRpRRaMI0sxlNxlIO10JxNp0NlXudSJJdL+TIuLI5XJ5EH59UFwoWYrQ5ltxJlhoM8qzUAA7MHSoGIPL5UA== – xanatos Jan 02 '21 at 10:40
  • I'll say there are some missing optimizations in Roslyn about closures... Take a loot at the previous sharplab, and try commenting the whole second if, and see how the closures are moved in the decompiled code – xanatos Jan 02 '21 at 10:41
  • Opened a bug on [Roslyn github](https://github.com/dotnet/roslyn/issues/50213) – xanatos Jan 02 '21 at 11:15

1 Answers1

4

(let's ignore the bug I've commented about... it happens only in Release mode)

The most important lesson here is that https://sharplab.io/ is the most useful tool for discovering how the code is compiled. You write some lines of code and then watch the decompiled code. You can the easily guess what the C# compiler is doing.

In general the C# compiler doesn't want to have two "places" for the same variable. It would be a nightmare to keep them synced. Let's make an example:

void Foo(int par, bool b, ref Action act)
{     
    if (b)
    {
        act = () => Console.WriteLine(par);
    }

    par = par + 1;
    Console.WriteLine(par);
}

this is "translated" to:

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

    internal void <Foo>b__0()
    {
        Console.WriteLine(par);
    }
}

private void Foo(int par, bool b, ref Action act)
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.par = par;
    if (b)
    {
        act = new Action(<>c__DisplayClass0_.<Foo>b__0);
    }
    <>c__DisplayClass0_.par++;
    Console.WriteLine(<>c__DisplayClass0_.par);
}

We can see that <>c__DisplayClass0_0 is instantiated at the beginning of the method and it "captures" the par variable. From the third line of the method onward, the par variable isn't used anymore and instead <>c__DisplayClass0_.par is used. Would it be possible to move the instantiation of <>c__DisplayClass0_ inside the if? No, because moving it inside the if wouldn't "capture" the changes done in the par++:

private void Foo(int par, bool b, ref Action act)
{
    if (b)
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.par = par;
        act = new Action(<>c__DisplayClass0_.<Foo>b__0);
    }
    par++;
    Console.WriteLine(par);
}

At the end of the method, par and <>c__DisplayClass0_.par would be different.

So in general a variable that must be closed must be put inside the hidden object supporting the closure before the first local function that uses it. To make it easier Roslyn simply put the variable inside the object at the moment of declaration:

void Foo(int par, bool b, ref Action act)
{
    par = par + 1;

    if (b)
    {
        act = () => Console.WriteLine(par);
    }

    par++;
    Console.WriteLine(par);
}

is translated to

private void Foo(int par, bool b, ref Action act)
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.par = par;
    <>c__DisplayClass0_.par++;
    if (b)
    {
        act = new Action(<>c__DisplayClass0_.<Foo>b__0);
    }
    <>c__DisplayClass0_.par++;
    Console.WriteLine(<>c__DisplayClass0_.par);
}

Here the first par++ could be done with the "real" par (saving up the time for a dereferencing of <>c__DisplayClass0_), and instead it is done to <>c__DisplayClass0_.par.

By creating a local variable inside the if, you use the fact that Roslyn will create/set up the hidden object when the variable is declared:

void Foo(int par, bool b, ref Action act)
{
    if (b)
    {
        int par2 = par;
        act = () => Console.WriteLine(par2);
    }

    par++;
    Console.WriteLine(par);
}

is translated to

private void Foo(int par, bool b, ref Action act)
{
    if (b)
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.par2 = par;
        act = new Action(<>c__DisplayClass0_.<Foo>b__0);
    }
    par++;
    Console.WriteLine(par);
}

Note that now par and <>c__DisplayClass0_.par2 are distinct variables, and will have different values, but you as a programmer decided it, so it is ok.

The this capture is different:

In the simplest case, when no other variables are captured, no hidden object is created:

int bar;

void Foo(int par, bool b, ref Action act)
{
    if (b)
    {
        act = () => Console.WriteLine(bar);
    }

    Console.WriteLine(par);
}

is translated to:

private int bar;

private void Foo(int par, bool b, ref Action act)
{
    if (b)
    {
        act = new Action(<Foo>b__1_0);
    }
    Console.WriteLine(par);
}

[CompilerGenerated]
private void <Foo>b__1_0()
{
    Console.WriteLine(bar);
}

But if variables are captured, then an hidden object is created that must contain the this:

int bar;

void Foo(int par, bool b, ref Action act)
{
    if (b)
    {
        act = () => Console.WriteLine(par + bar);
    }

    Console.WriteLine(par);
}

is translated to:

[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
    public int par;

    public C <>4__this;

    internal void <Foo>b__0()
    {
        Console.WriteLine(par + <>4__this.bar);
    }
}

private int bar;

private void Foo(int par, bool b, ref Action act)
{
    <>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
    <>c__DisplayClass1_.par = par;
    <>c__DisplayClass1_.<>4__this = this;
    if (b)
    {
        act = new Action(<>c__DisplayClass1_.<Foo>b__0);
    }
    Console.WriteLine(<>c__DisplayClass1_.par);
}

Now, the delegate will contain a reference to this (<>c__DisplayClass1_.<>4__this), so the lifetime of this will be >= the lifetime of the delegate (the GC can't collect the an instance of the class of Foo until all the delegates generated by that instance.Foo are collected).

Note that we could:

[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
    public int par;
}

[CompilerGenerated]
private sealed class <>c__DisplayClass1_1
{
    public int bar2;

    public <>c__DisplayClass1_0 CS$<>8__locals1;

    internal void <Foo>b__0()
    {
        Console.WriteLine(CS$<>8__locals1.par + bar2);
    }
}

private int bar;

private void Foo(int par, bool b, ref Action act)
{
    <>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
    <>c__DisplayClass1_.par = par;
    if (b)
    {
        <>c__DisplayClass1_1 <>c__DisplayClass1_2 = new <>c__DisplayClass1_1();
        <>c__DisplayClass1_2.CS$<>8__locals1 = <>c__DisplayClass1_;
        <>c__DisplayClass1_2.bar2 = bar;
        act = new Action(<>c__DisplayClass1_2.<Foo>b__0);
    }
    Console.WriteLine(<>c__DisplayClass1_.par);
}

No this captured here (but bar and bar2 are now distinct!).

In the example given, they capture the local field _factory probably for this reason. _factory is a readonly field, so it won't change for the entire lifetime of the object, so they can close directly in it instead of closing around this.

As a sidenote there is another problem with capturing variables that begin their lifetime at different nesting level of scope. ({ int a; { int b; } }): Roslyn could create a multilevel hidden object (so multiple hidden objects that reference one another). It is seen in one of the last examples, where par is inside <>c__DisplayClass1_0 while bar2 is inside <>c__DisplayClass1_1.

xanatos
  • 109,618
  • 12
  • 197
  • 280