6

I'm analysing OptionsManager.Get(string name):

/// <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;
}

What does the following actually mean?

Avoid closure on fast path by storing state into scoped locals

What is a "fast path"?

What is the benefit of creating a local variable pointing to _factory, compared to just using _factory?

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • 2
    My guess is that since `localFactory` is scoped to the block of the slower path (cache miss), it won't allocate the object that captures `localFactory` unless it enters that block, whereas capturing `_factory` requires it to be captured at the start of the method (because it has class instance scope), before it checks the cache, thereby being allocated every time, even when it's not needed. Just a guess. – madreflection Jan 01 '21 at 02:48
  • Does this answer your question? [What are 'closures' in .NET?](https://stackoverflow.com/questions/428617/what-are-closures-in-net) – Charlieface Jan 01 '21 at 02:50

1 Answers1

6

@madreflections synopsis is basically correct.

The longer story

A lambda is just an anonymous method (which in turn is just a method without a name), C# introduced lamdas as a concise way of creating anonymous methods. However, if the anonymous method needs to access free variables (variables which are not part of the anonymous methods parameters or local scope) it needs a way to capture them. This is done via a closure.

A closure is basically just a class the compiler generates to take a copy of free variables to access in the anonymous method.

Note : The compiler may not always generate a closure even with free variables, sometimes it can just generate an instance method in the case of referencing instance fields or properties. However, if it does need to capture a variable a closure is generated.

When a closure is generated the compiler not only creates the code for the closure class, it needs to instantiate that class and assign any variables. Depending on the scope of those variables a decision will be made to determine where in the code to initialize the class and plumb up the dependencies.

Since closures are just plain old classes, they need to be instantiated which in turn is an allocation and as such comes with a small overhead. In the case of the source code supplied, the developers weighed up the cost of that overhead and decided to make a small efficiency.

This efficiency was made knowing the fact the closure will be instantiated at the outer most scope it "needs" to capture the free variables. The compiler uses fall-back approach based on scope and is a bit more complicated. However, by assigning locals in the way shown in the immediate inner scope, the compiler knows it's safe to create the closure in that scope. Which in turn means the small overhead of creation is limited to that branch of the code-block scope of the if statement.

Some nonsensical example

// instance field
readonly Version _something = new Version();

public virtual void Test1(int someValue)
{ 
   // some weird branch
   if (DateTime.Now == DateTime.MaxValue)
   {
      Method(() => _something.Build + someValue);
   }
}

public virtual void Test2(int someValue)
{
   // some weird branch
   if (DateTime.Now == DateTime.MaxValue)
   {
      // some locals to let the compiler create the closure in the immediate scope
      var localSomething = _something;
      var localValue = someValue;
      Method(() => localSomething.Build + localValue);
   }
}

public virtual void Test3(int someValue)
{
   // some weird branch
   if (DateTime.Now == DateTime.MaxValue)
   {
      // closure needed, it can just create an anonymous method in the class
      // and reference _something directly
      Method(() => _something.Build);
   }
}

// something with a delegate parameter.
private int Method(Func<int> func) => func();

After compilation

Note : This is rough interoperation of what the compiler has done. For all the gory details look at this Example

public class CallingClass
{
   [CompilerGenerated]
   private sealed class Closure1
   {
      public CallingClass copyOfCallingClass;

      public int someValue;

      internal int Method()
      {
         return copyOfCallingClass._something.Build + someValue;
      }
   }

   [CompilerGenerated]
   private sealed class Closure2
   {
      public Version localSomething;

      public int localValue;

      internal int Method()
      {
         return localSomething.Build + localValue;
      }
   }

   [CompilerGenerated]
   private int NewMethod()
   {
      return _something.Build;
   }

   private readonly Version _something = new Version();

   public virtual void Test1(int someValue)
   {
      // generated closure plumbing
      Closure1 Closure1 = new Closure1();
      Closure1.copyOfCallingClass = this;
      Closure1.someValue = someValue;
      if (DateTime.Now == DateTime.MaxValue)
      {
         Method(new Func<int>(Closure1.Method));
      }
   }

   public virtual void Test2(int someValue)
   {
      if (DateTime.Now == DateTime.MaxValue)
      {
         // generated closure plumbing
         Closure2 closure2 = new Closure2();
         closure2.localSomething = _something;
         closure2.localValue = someValue;
         Method(new Func<int>(closure2.Method));
      }
   }

   public virtual void Test3(int someValue)
   {
      if (DateTime.Now == DateTime.MaxValue)
      {
         // pointer to the new generated method
         Method(new Func<int>(NewMethod));
      }
   }

   private int Method(Func<int> func)
   {
      return func();
   }
}

Disclaimer : Although closures are a fairly pedestrian topic, how the compiler chooses to achieve this and the rules thereof is in itself nuanced, and as such would require an in-depth dive into the specifications. This was just intended as a high level summary.

halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • I had to read it 2x but it finally makes sense now. Thanks for a great answer. – David Klempfner Jan 01 '21 at 11:59
  • Do you know why the closure would be created before the if statement? I'm not sure of the implementation details, but couldn't the closure over _factory and name be created in the if statement? – David Klempfner Jan 01 '21 at 12:52
  • @DavidKlempfner I don't know the answer off hand, the exact rules and implementation will be buried in the CLR. I have come across them once or twice in my travels though, when I have time tomorrow ill try and find an authoritative source for them. Though we do know, if we localize the variables, the closure will be implemented in the local scope – TheGeneral Jan 01 '21 at 13:06