通过将状态存储到作用域内的局部变量中来避免在快速路径上关闭

Avoid closure on fast path by storing state into scoped locals

提问人:David Klempfner 提问时间:1/1/2021 最后编辑:Camilo TerevintoDavid Klempfner 更新时间:1/3/2021 访问量:592

问:

我正在分析OptionsManager.Get(字符串名称):

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

以下内容究竟意味着什么?

通过将状态存储到作用域内的局部变量中来避免在快速路径上关闭

什么是“快速通道”?

与仅使用 相比,创建指向 的局部变量有什么好处?_factory_factory

C# .net-core 闭包

评论

2赞 madreflection 1/1/2021
我的猜测是,由于作用域限定为较慢路径的块(缓存未命中),因此除非它进入该块,否则它不会分配捕获的对象,而捕获需要在方法开始时捕获它(因为它具有类实例范围),然后再检查缓存,因此每次都分配, 即使不需要。只是一个猜测。localFactorylocalFactory_factory
0赞 Charlieface 1/1/2021
这回答了你的问题吗?什么是 .NET 中的“闭包”?

答:

6赞 TheGeneral 1/1/2021 #1

@madreflections概要基本上是正确的。

长篇大论

lambda 只是一个匿名方法(而它又只是一个没有名称的方法),C# 引入了 lamdas 作为创建匿名方法的简洁方法。但是,如果匿名方法需要访问自由变量(不属于匿名方法参数或局部范围的变量),则需要一种方法来捕获它们。这是通过闭合完成的。

闭包基本上只是编译器生成的一个类,用于获取自由变量的副本以在匿名方法中访问。

注意:即使使用自由变量,编译器也可能并不总是生成闭包,有时在引用实例字段或属性的情况下,它只能生成一个实例方法。但是,如果它确实需要捕获变量,则会生成闭包。

生成闭包时,编译器不仅要为闭包类创建代码,还需要实例化该类并分配任何变量。根据这些变量的作用,将决定在代码中的哪个位置初始化类并探测依赖项。

由于闭包只是普通的旧类,因此需要实例化它们,这反过来又是一种分配,因此会产生很小的开销。在提供的源代码的情况下,开发人员权衡了开销的成本,并决定降低效率。

这种效率是知道闭包将在它“需要”捕获自由变量的最外层范围内实例化的。编译器使用基于范围的回退方法,并且稍微复杂一些。但是,通过以直接内部作用域中所示的方式分配局部变量,编译器知道在该作用域中创建闭包是安全的。这反过来又意味着创建的小开销仅限于语句代码块范围的那个分支。if

一些荒谬的例子

// 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();

编译后

注意:这是编译器所做工作的粗略互操作。有关所有血腥的细节,请查看此示例

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();
   }
}

免责声明:尽管闭包是一个相当平淡的话题,但编译器如何选择实现这一点及其规则本身就很微妙,因此需要深入研究规范。这只是一个高层次的总结。

评论

0赞 David Klempfner 1/1/2021
我不得不读了 2 遍,但现在终于有意义了。感谢您的精彩回答。
0赞 David Klempfner 1/1/2021
你知道为什么在 if 语句之前创建闭包吗?我不确定实现细节,但是不能在 if 语句中创建对 _factory 和 name 的闭包吗?
0赞 TheGeneral 1/1/2021
@DavidKlempfner我不知道答案,但确切的规则和实现将埋没在 CLR 中。不过,我在旅途中遇到过一两次,当我明天有时间时,我会尝试为它们找到一个权威的来源。虽然我们确实知道,如果我们对变量进行本地化,闭包将在局部范围内实现