Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid closure on fast path by storing state into scoped locals

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?

like image 931
David Klempfner Avatar asked Jan 01 '21 02:01

David Klempfner


1 Answers

@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.

like image 159
TheGeneral Avatar answered Oct 10 '22 19:10

TheGeneral