为什么异步状态机 类(而不是结构)在 Roslyn 中?
Why are async state machines classes (and not structs) in 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 生成一个 class(而不是结构)。如果我没记错的话,旧编译器(我猜是 CTP2012)中 async/await 支持的第一个实现也生成了 classes,然后出于性能原因将其更改为结构。 (在某些情况下,您可以完全避免装箱和堆分配......)(参见this)
有谁知道为什么在 Roslyn 中再次更改此内容? (我对此没有任何问题,我知道这个更改是透明的,不会改变任何代码的行为,我只是好奇)
编辑:
@Damien_The_Unbeliever 的回答(和源代码 :))恕我直言,解释了一切。所描述的 Roslyn 行为仅适用于调试构建(由于评论中提到的 CLR 限制,这是必需的)。在 Release 中,它还生成一个结构(具有它的所有好处……)。所以这似乎是一个非常聪明的解决方案,可以同时支持 Edit and Continue 和更好的生产性能。有趣的东西,感谢所有参与的人!
我对此没有任何先见之明,但由于最近 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
s 有一些吸引力,但允许 Edit and Continue 在 async
方法中工作的巨大胜利显然被选为更好的选择。
对于这样的事情很难给出明确的答案(除非编译器团队的人加入 :)),但有几点您可以考虑:
结构的性能 "bonus" 始终是一种权衡。基本上,您会得到以下内容:
- 值语义
- 可能的堆栈(甚至寄存器?)分配
- 避免间接访问
这在 await 案例中意味着什么?好吧,实际上……没什么。状态机在堆栈上的时间非常短——请记住,await
有效地执行了 return
,因此方法堆栈终止;状态机必须保存在某个地方,并且 "somewhere" 肯定在堆上。堆栈生命周期不适合异步代码:)
除此之外,状态机违反了一些定义结构的良好准则:
struct
s 最多应该是 16 字节大 - 状态机包含两个指针,它们自己在 64 位上整齐地填充 16 字节限制。除此之外,还有状态本身,所以它超越了 "limit"。这不是一个 大 交易,因为它很可能只通过引用传递,但请注意这不太适合结构的用例 - 一个基本上是引用类型的结构。
struct
s 应该是不可变的——好吧,这可能不需要太多评论。这是一个 状态机 。同样,这没什么大不了的,因为该结构是自动生成的代码并且是私有的,但是...
struct
s 在逻辑上应该表示单个值。绝对不是这里的情况,但是从一开始就有一个可变状态就已经有点像这样了。
- 它不应该经常装箱 - 这里不是问题,因为我们无处不在使用泛型。状态最终在堆上的某个地方,但至少它没有被装箱(自动)。同样,它仅在内部使用这一事实使得这几乎是无效的。
当然,所有这些都是在没有关闭的情况下。当您有遍历 await
的局部变量(或字段)时,状态会进一步膨胀,从而限制使用结构的有用性。
考虑到所有这些,class 方法绝对更简洁,我不希望使用 struct
来显着提高性能。所有涉及的对象都有相似的生命周期,所以提高内存性能的唯一方法是使它们 all struct
s (例如存储在一些缓冲区中) -当然,这在一般情况下是不可能的。大多数情况下,您首先使用 await
(即一些异步 I/O 工作)已经涉及其他 classes - 例如,数据缓冲区,字符串......您不太可能 await
只是 returns 42
而不进行任何堆分配。
最后,我想说的是,您真正看到真正性能差异的唯一地方是基准测试。至少可以说,针对基准进行优化是一个愚蠢的想法...
让我们考虑一下这个非常简单的异步方法:
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 生成一个 class(而不是结构)。如果我没记错的话,旧编译器(我猜是 CTP2012)中 async/await 支持的第一个实现也生成了 classes,然后出于性能原因将其更改为结构。 (在某些情况下,您可以完全避免装箱和堆分配......)(参见this)
有谁知道为什么在 Roslyn 中再次更改此内容? (我对此没有任何问题,我知道这个更改是透明的,不会改变任何代码的行为,我只是好奇)
编辑:
@Damien_The_Unbeliever 的回答(和源代码 :))恕我直言,解释了一切。所描述的 Roslyn 行为仅适用于调试构建(由于评论中提到的 CLR 限制,这是必需的)。在 Release 中,它还生成一个结构(具有它的所有好处……)。所以这似乎是一个非常聪明的解决方案,可以同时支持 Edit and Continue 和更好的生产性能。有趣的东西,感谢所有参与的人!
我对此没有任何先见之明,但由于最近 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
s 有一些吸引力,但允许 Edit and Continue 在 async
方法中工作的巨大胜利显然被选为更好的选择。
对于这样的事情很难给出明确的答案(除非编译器团队的人加入 :)),但有几点您可以考虑:
结构的性能 "bonus" 始终是一种权衡。基本上,您会得到以下内容:
- 值语义
- 可能的堆栈(甚至寄存器?)分配
- 避免间接访问
这在 await 案例中意味着什么?好吧,实际上……没什么。状态机在堆栈上的时间非常短——请记住,await
有效地执行了 return
,因此方法堆栈终止;状态机必须保存在某个地方,并且 "somewhere" 肯定在堆上。堆栈生命周期不适合异步代码:)
除此之外,状态机违反了一些定义结构的良好准则:
struct
s 最多应该是 16 字节大 - 状态机包含两个指针,它们自己在 64 位上整齐地填充 16 字节限制。除此之外,还有状态本身,所以它超越了 "limit"。这不是一个 大 交易,因为它很可能只通过引用传递,但请注意这不太适合结构的用例 - 一个基本上是引用类型的结构。struct
s 应该是不可变的——好吧,这可能不需要太多评论。这是一个 状态机 。同样,这没什么大不了的,因为该结构是自动生成的代码并且是私有的,但是...struct
s 在逻辑上应该表示单个值。绝对不是这里的情况,但是从一开始就有一个可变状态就已经有点像这样了。- 它不应该经常装箱 - 这里不是问题,因为我们无处不在使用泛型。状态最终在堆上的某个地方,但至少它没有被装箱(自动)。同样,它仅在内部使用这一事实使得这几乎是无效的。
当然,所有这些都是在没有关闭的情况下。当您有遍历 await
的局部变量(或字段)时,状态会进一步膨胀,从而限制使用结构的有用性。
考虑到所有这些,class 方法绝对更简洁,我不希望使用 struct
来显着提高性能。所有涉及的对象都有相似的生命周期,所以提高内存性能的唯一方法是使它们 all struct
s (例如存储在一些缓冲区中) -当然,这在一般情况下是不可能的。大多数情况下,您首先使用 await
(即一些异步 I/O 工作)已经涉及其他 classes - 例如,数据缓冲区,字符串......您不太可能 await
只是 returns 42
而不进行任何堆分配。
最后,我想说的是,您真正看到真正性能差异的唯一地方是基准测试。至少可以说,针对基准进行优化是一个愚蠢的想法...