ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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。 (在某些情况下,您可以完全避免装箱和堆分配……)(参见 this

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

编辑:

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

我怀疑他们认为复杂性(re mutable structs)不值得。 async 方法几乎总是有一个真正的异步点 - 一个产生控制的 await,这将要求结构体无论如何都要装箱。我相信结构只会减轻碰巧同步运行的 async 方法的内存压力。

D
Damien_The_Unbeliever

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

在这里,在 line 60 of the AsyncRewriter,我们发现:

// 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 有一些吸引力,但允许 Edit and Continueasync 方法中工作的巨大胜利显然被选为更好的选择。


非常好抓!基于此,我还发现:这只发生在您在调试中构建它时(有意义,那是您执行 EnC..),但在 Release 中它们创建了一个结构(显然 EnableEditAndContinue 在这种情况下是错误的.. .)。顺便提一句。我也尝试查看代码,但没有找到。非常感谢!
L
Luaan

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

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

值语义

可能的堆栈(甚至可能是寄存器?)分配

避免间接

这在 await 案例中意味着什么?嗯,其实……什么都没有。状态机在堆栈上的时间很短——记住,await 有效地执行了 return,所以方法堆栈死了;状态机必须保存在某处,而“某处”肯定在堆上。堆栈生命周期不适合异步代码:)

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

结构最多应为 16 字节大 - 状态机包含两个指针,它们自己巧妙地填充 64 位上的 16 字节限制。除此之外,还有状态本身,所以它超过了“限制”。这没什么大不了的,因为它很可能只通过引用传递,但请注意这不太适合结构的用例 - 一个基本上是引用类型的结构。

结构应该是不可变的——好吧,这可能不需要太多评论。这是一个状态机。同样,这没什么大不了的,因为结构是自动生成的代码和私有的,但是......

结构应在逻辑上表示单个值。绝对不是这里的情况,但这已经是从一开始就具有可变状态的结果。

它不应该经常装箱——这不是问题,因为我们到处都在使用泛型。状态最终在堆上的某个地方,但至少它没有被装箱(自动)。同样,它仅在内部使用的事实使得这几乎是无效的。

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

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

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


当您可以去阅读源代码时,您并不总是需要编译器团队的成员,他们留下了有用的评论:-)
@Damien_The_Unbeliever 是的,这绝对是一个很棒的发现,我已经赞成你的回答:P
在代码不异步运行的情况下,结构有很大帮助,例如数据已经在缓冲区中。