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

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

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,我猜)中的asynchronous/等待支持的第一个实现也生成了类,然后从性能原因改为结构。 (在某些情况下,你可以完全避免拳击和堆分配…)(看到这个 )

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

编辑:

来自@Damien_The_Unbeliever(和源代码:))的答案imho解释了一切。 Roslyn描述的行为只适用于debugging版本(因为评论中提到的CLR限制,这是需要的)。 在释放它也生成一个结构(具有所有的好处..)。 所以这似乎是一个非常聪明的解决scheme来支持编辑和继续,并在生产中获得更好的性能。 有趣的东西,谢谢大家谁参与!

我对此没有任何预见,但是由于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; 

所以,虽然有一些使用struct的吸引力,但允许Edit和Continue在async方法中工作的巨大胜利显然被选为更好的select。

很难给出这样的明确答案(除非编译团队的某个人下降:)),但有几点你可以考虑:

结构的performance“奖金”总是一个折衷。 基本上,你得到以下几点:

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

这是什么意思等待案件? 那么,其实……什么都没有。 状态机在堆栈上只有很短的时间 – 记住, await有效的return ,所以方法堆栈死了; 状态机必须保存在某个地方,而“某个地方”肯定是堆在一起的。 堆栈生存期不适合asynchronous代码:)

除此之外,状态机违反了定义结构的一些好的指导方针:

  • struct s应该最多16个字节 – 状态机包含两个指针,它们自己在64位上整齐地填充16个字节的限制。 除此之外,还有国家本身,所以超越了“极限”。 这不是什么问题,因为它很可能只是通过引用而传递,但是请注意,这并不完全适合结构的用例 – 一个基本上是引用types的结构。
  • struct s应该是不可变的 – 好吧,这可能不需要太多的评论。 这是一个状态机 。 同样,这不是一个大问题,因为结构是自动生成的代码和私人的,但…
  • struct应该在逻辑上代表一个单一的值。 这里肯定不是这样的情况,但是从一开始就有一个可变的状态。
  • 它不应该经常被装箱 – 这里不是问题,因为我们到处都在使用generics。 这个状态最终是堆在某个地方,但至less它没有被装箱(自动)。 再一次,它只在内部使用的事实使得这非常无效。

当然,这一切都是在没有closures的情况下。 当你有遍历await的locals(或字段)时,状态会进一步膨胀,限制使用struct的有效性。

考虑到所有这一切,类的方法绝对清洁,我不希望从使用struct代替任何明显的性能提高。 所有涉及的对象具有相似的生命周期,所以提高内存性能的唯一方法就是使它们全部 struct (例如存储在某个缓冲区中) – 当然这在一般情况下是不可能的。 大多数情况下,你首先会使用await (也就是说,一些asynchronousI / O工作)已经涉及到其他类 – 例如,数据缓冲区,string……这是不太可能的,你会await一些简单的返回42没有做任何堆分配。

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