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

gre*_*pos 86 .net c# language-lawyer roslyn

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

static async Task myMethodAsync() 
{
    await Task.Delay(500);
}
Run Code Online (Sandbox Code Playgroud)

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

private struct <myMethodAsync>d__0 : IAsyncStateMachine
{  
    ...
    void IAsyncStateMachine.MoveNext()
    {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

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

private sealed class <myMethodAsync>d__1 : IAsyncStateMachine
{
    ...
    void IAsyncStateMachine.MoveNext()
    {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,Roslyn生成一个类(而不是结构).如果我没记错的话,旧编译器中的async/await支持的第一个实现(我认为是CTP2012)也生成了类,然后由于性能原因将其更改为struct.(在某些情况下,你可以完全避免拳击和堆分配......)(见这个)

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

编辑:

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

Dam*_*ver 110

我对此没有任何预知,但由于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;
Run Code Online (Sandbox Code Playgroud)

因此,尽管使用structs 有一些吸引力,但允许编辑和继续async方法中工作的重大胜利显然被选为更好的选择.

  • 很好抓!在此基础上这里是我还发现:当你在调试建立它这只是发生(是有道理的,这时候你做ENC ..),但在释放前,他们创建一个结构(显然EnableEditAndContinue在这种情况下,假.. ).顺便说一句.我也尝试查看代码,但没有找到这个.非常感谢! (18认同)

Lua*_*aan 5

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

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

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

这在等待情况下意味着什么?嗯,实际上……没什么。状态机在堆栈上的时间很短 - 请记住,await实际上执行了 a return,因此方法堆栈死亡;状态机必须保存在某个地方,而那个“某个地方”肯定是在堆上。堆栈生命周期不太适合异步代码:)

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

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

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

考虑到这一切,类方法绝对更干净,我不认为使用 astruct来代替会带来任何明显的性能提升。所有涉及的对象都有相似的生命周期,因此提高内存性能的唯一方法是使所有对象都成为structs (例如,存储在某个缓冲区中) - 当然,这在一般情况下是不可能的。大多数情况下,您await首先使用的情况(即某些异步 I/O 工作)已经涉及其他类 - 例如,数据缓冲区、字符串...您不太可能会await简单地返回42而不执行任何操作堆分配。

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

  • @Damien_The_Unknowner 是的,这绝对是一个很棒的发现,我已经对你的答案投了赞成票:P (4认同)
  • 在代码不异步运行的情况下,该结构有很大帮助,例如数据已经在缓冲区中。 (2认同)