为什么在Roslyn中使用异步状态机类(而不是结构)?


86

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

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()
    {
        ...
    }
}

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

有谁知道为什么在罗斯林再次改变了这一点?(对此我没有任何问题,我知道此更改是透明的,不会更改任何代码的行为,我很好奇)

编辑:

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


2
我怀疑他们认为复杂性(可变结构)不值得。async方法几乎总是具有真正的异步点-await产生控制点,无论如何都将需要对结构进行装箱。我相信结构只会减轻async碰巧同步运行的方法的内存压力。
Stephen Cleary

Answers:


112

我对此没有任何预见,但是由于Roslyn这些天都是开源的,因此我们可以遍历代码进行解释。

在这里,在AsyncRewriter的第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;

因此,尽管使用structs有一些吸引力,但显然选择允许Edit和Continueasync方法中工作的巨大胜利。


18
很好抓!基于此,我也发现了这一点:仅当在调试中构建它时才有意义(有意义的是,这是在执行EnC ..时),但是在Release中,它们创建了一个结构(显然,EnableEditAndContinue在这种情况下为false。 )。顺便说一句。我也尝试查看代码,但没有找到。非常感谢!
gregkalapos15年

3

对于像这样的事情很难给出明确的答案(除非编译器团队中的人加入:)),但是您可以考虑以下几点:

结构的性能“奖励”始终是一种折衷。基本上,您得到以下内容:

  • 值语义
  • 可能的堆栈(甚至可以注册?)分配
  • 避免间接

在等待案件中这意味着什么?好吧,实际上...什么都没有。状态机在堆栈上只有很短的时间-请记住,await有效地执行a return,因此方法堆栈消失;状态机必须保留在某个地方,并且“某个地方”肯定在堆上。堆栈生存期不太适合异步代码:)

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

  • structs的最大长度应为16个字节-状态机包含两个指针,它们本身可以在64位上完全填充16字节的限制。除此之外,还有状态本身,因此它超出了“限制”。这没什么大不了的,因为它很可能只是通过引用传递的,但是请注意,这与结构的用例不太吻合-结构基本上是引用类型。
  • structs应该是不变的-嗯,这可能不需要太多注释。这是一个状态机。同样,这没什么大不了的,因为该结构是自动生成的代码并且是私有的,但是...
  • structs在逻辑上应该代表一个值。绝对不是这里的情况,但是从一开始就具有可变状态已经是一种跟随。
  • 不应经常将其装箱-这里不是问题,因为我们到处都在使用泛型。状态最终位于堆上的某个位置,但至少没有(自动)装箱。同样,仅在内部使用它的事实使它几乎无效。

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

考虑到所有这些,类方法绝对是更干净的方法,并且我不希望通过使用struct替代方法获得任何明显的性能提升。所有涉及的对象都具有相似的生存期,因此提高内存性能的唯一方法是使它们全部struct(例如,存储在某个缓冲区中)-当然,在通常情况下这是不可能的。在大多数情况下,您首先要使用await的(即一些异步I / O工作)已经涉及其他类-例如,数据缓冲区,字符串...不太可能您会await返回一些42不做任何事情而直接返回的内容堆分配。

最后,我要说的是,您真正能看到真正的性能差异的唯一地方是基准测试。至少可以说,针对基准进行优化是一个愚蠢的主意...


当您可以去阅读源代码时,您并不总是需要编译器团队的成员,而他们留下了有用的评论:-)
Damien_The_Unbeliever 2015年

3
@Damien_The_Unbeliever是的,这绝对是一个不错的发现,我已经赞成您的回答了:P
六安2015年

1
在代码不异步运行的情况下,该结构很有帮助,例如,数据已经在缓冲区中。
伊恩·林格罗斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.