为什么Roslyn中有异步状态机类(而不是结构)

本文关键字:结构 Roslyn 异步 状态机 为什么 | 更新日期: 2023-09-27 18:24:00

让我们考虑一下这个非常简单的异步方法:

static async Task myMethodAsync() 
{
    await Task.Delay(500);
}

当我用VS2013(Roslyn之前的编译器)编译它时,生成的状态机是一个结构。

private struct <myMethodAsync>d__0 : IAsyncStateMachine
{  
    ...
    void IAsyncStateMachine.MoveNext()
    {
        ...
    }
}

当我用VS2015(Roslyn)编译它时,生成的代码是这样的:

private sealed class <myMethodAsync>d__1 : IAsyncStateMachine
{
    ...
    void IAsyncStateMachine.MoveNext()
    {
        ...
    }
}

正如您所看到的,Roslyn生成了一个类(而不是结构)。如果我没有记错的话,旧编译器中异步/等待支持的第一个实现(我猜是CTP2012)也生成了类,然后出于性能原因将其更改为struct。(在某些情况下,你可以完全避免装箱和堆分配…)(见此)

有人知道罗斯林为什么会再次改变吗?(我对此没有任何问题,我知道这个更改是透明的,不会改变任何代码的行为,我只是好奇)

编辑:

来自@Damien_The_Unbeliever的答案(以及源代码:))imho解释了一切。Roslyn所描述的行为仅适用于调试构建(由于注释中提到的CLR限制,这是必需的)。在Release中,它还生成了一个结构(具有..的所有优点)。因此,这似乎是一个非常聪明的解决方案,可以同时支持编辑和继续,并在生产中获得更好的性能。有趣的东西,感谢所有参与的人!

为什么Roslyn中有异步状态机类(而不是结构)

我对此一无所知,但由于Roslyn现在是开源的,我们可以在代码中寻找解释。

在异步重写器的第60行,我们发现:

// The CLR doesn't support adding fields to structs, so in order to enable EnC in an async method we need to generate a class.
var typeKind = compilationState.Compilation.Options.EnableEditAndContinue ? TypeKind.Class : TypeKind.Struct;

因此,虽然使用struct有一些吸引力,但允许"编辑并继续"在async方法中工作的巨大胜利显然被选为更好的选择。

对于这样的事情很难给出明确的答案(除非编译器团队中有人来:),但有几点你可以考虑:

structs的性能"红利"总是一种权衡。基本上,你会得到以下内容:

  • 值语义
  • 可能的堆栈(甚至可能是寄存器?)分配
  • 避免间接

这在等待案件中意味着什么?实际上。。。没有什么状态机在堆栈上的时间很短——记住,await有效地执行了return,因此方法堆栈死亡;状态机必须保存在某个地方,而"某个地方"肯定是堆上的。堆栈寿命不适合异步代码:)

除此之外,状态机违反了定义结构的一些良好准则:

  • struct最多应该有16字节大——状态机包含两个指针,它们自己在64位上巧妙地填充了16字节的限制。除此之外,还有国家本身,所以它超过了"极限"。这不是一个大的问题,因为它很可能只通过引用传递,但请注意,这不太适合structs的用例——一个基本上是引用类型的结构
  • struct应该是不可变的——好吧,这可能不需要太多注释。这是一个状态机。同样,这不是什么大不了的,因为结构是自动生成的代码和私有的,但是
  • struct s应在逻辑上表示单个值。这里的情况肯定不是这样,但这已经是从一开始就有可变状态开始的
  • 它不应该经常被装箱——这不是问题,因为我们在任何地方都使用泛型。状态最终是堆中的某个位置,但至少它没有被装箱(自动)。同样,它只在内部使用的事实使这一点变得非常空洞

当然,所有这些都是在没有关闭的情况下发生的。当您有遍历await s的局部变量(或字段)时,状态会进一步膨胀,从而限制了使用结构的有用性。

考虑到所有这些,类方法肯定更干净,而且我不希望使用struct会显著提高性能。所有涉及的对象都有相似的生存期,因此提高内存性能的唯一方法是将所有对象设为structs(例如,存储在某个缓冲区中)——当然,这在一般情况下是不可能的。大多数情况下,首先使用await(也就是说,一些异步I/O工作)已经涉及到其他类,例如,数据缓冲区、字符串。。。您不太可能只返回42而不进行任何堆分配的await

最后,我想说,你真正能看到真正性能差异的地方只有基准测试。至少可以说,为基准进行优化是一个愚蠢的想法。。。