Roslynに非同期ステートマシンクラス(構造体ではない)があるのはなぜですか?


87

この非常に単純な非同期メソッドについて考えてみましょう。

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だと思います)でのasync / awaitサポートの最初の実装もクラスを生成し、パフォーマンス上の理由から構造体に変更されたことを正しく覚えていれば。(場合によっては、ボクシングとヒープの割り当てを完全に回避できます…)(これを参照)

Roslynでこれが再び変更された理由を誰かが知っていますか?(これに関しては問題ありません。この変更は透過的であり、コードの動作を変更しないことを知っています。興味があります)

編集:

@Damien_The_Unbeliever(およびソースコード:))imhoからの回答がすべてを説明しています。説明されているRoslynの動作は、デバッグビルドにのみ適用されます(コメントに記載されているCLRの制限のために必要です)。リリースでは、構造体も生成します(そのすべての利点があります)。したがって、これは編集と続行の両方をサポートし、本番環境でのパフォーマンスを向上させるための非常に賢いソリューションのようです。興味深いもの、参加してくれたすべての人に感謝します!


2
複雑さ(可変構造体)は価値がないと彼らが判断したのではないかと思います。asyncほとんどの場合、メソッドには真の非同期ポイントawaitがあります。これは制御を生成するため、とにかく構造体をボックス化する必要があります。構造体は、たまたま同期して実行されたメソッドのメモリプレッシャーを軽減するだけだと思いますasync
スティーブンクリアリー2015年

回答:


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を使用することにはいくつかの魅力がありますが、メソッド内で編集と続行を許可するという大きなメリットは、async明らかに優れたオプションとして選択されました。


18
とても良いキャッチ!そしてこれに基づいて私も発見しました:これはデバッグでビルドしたときにのみ発生します(理にかなっています、それはEnCを実行するときです..)が、リリースでは構造体を作成します(その場合は明らかにEnableEditAndContinueはfalseです。 。)。ところで。コードも調べてみましたが、見つかりませんでした。どうもありがとう!
gregkalapos 2015年

3

このようなものに対して決定的な答えを出すのは難しいですが(コンパイラチームの誰かが立ち寄らない限り:))、考慮できるいくつかのポイントがあります:

構造体のパフォーマンスの「ボーナス」は常にトレードオフです。基本的に、次のようになります。

  • 値のセマンティクス
  • 可能なスタック(おそらくレジスタ?)割り当て
  • 間接参照の回避

これはawaitの場合はどういう意味ですか?まあ、実際には...何も。ステートマシンがスタック上にある期間はごくわずかです。覚えておいてください。await事実上return、を実行するため、メソッドスタックは停止します。ステートマシンはどこかに保存する必要があり、その「どこか」は間違いなくヒープ上にあります。スタックの存続期間は非同期コードにうまく適合しません:)

これとは別に、ステートマシンは構造体を定義するためのいくつかの優れたガイドラインに違反しています。

  • structsは最大で16バイトの大きさである必要があります。ステートマシンには2つのポインタが含まれており、それら自体が64ビットの16バイトの制限を適切に満たします。それとは別に、状態自体があるので、それは「限界」を超えています。これは、参照によってのみ渡される可能性が非常に高いため、大したことではありませんが、基本的に参照型である構造体のユースケースに完全に適合しないことに注意してください。
  • structsは不変である必要があります-まあ、これはおそらく多くのコメントを必要としません。それはだ、ステートマシン。繰り返しますが、構造体は自動生成されたコードでプライベートであるため、これは大したことではありませんが...
  • structsは、論理的に単一の値を表す必要があります。ここでは絶対に当てはまりませんが、そもそも可変状態を持っていることから、すでにそのようなことが起こります。
  • 頻繁にボックス化するべきではありません-どこでもジェネリックを使用しているので、ここでは問題ありません。状態は最終的にはヒープのどこかにありますが、少なくとも(自動的に)ボックス化されていません。繰り返しますが、内部でのみ使用されるという事実により、これはほとんど無効になります。

そしてもちろん、これはすべてクロージャがない場合です。awaitsをトラバースするローカル(またはフィールド)がある場合、状態はさらに膨張し、構造体を使用することの有用性が制限されます。

これらすべてを考慮すると、クラスアプローチは間違いなくクリーンであり、struct代わりに使用することによる顕著なパフォーマンスの向上は期待できません。関連するすべてのオブジェクトの有効期間は同じであるため、メモリパフォーマンスを向上させる唯一の方法は、すべてのオブジェクトを作成することですstruct(たとえば、何らかのバッファに格納する)。これは、もちろん、一般的なケースでは不可能です。そしてawait、そもそも使用するほとんどの場合(つまり、一部の非同期I / O作業)には、すでに他のクラス(データ・バッファー、文字列など)が含まれています...await何も42せずに単に戻るものはほとんどありません。ヒープ割り当て。

結局、実際のパフォーマンスの違いが実際に見られるのはベンチマークだけだと思います。そして、ベンチマークの最適化は、控えめに言っても、ばかげた考えです...


ソースにアクセスして読むことができる場合、コンパイラチームのメンバーは必ずしも必要ではなく、有益なコメントが残されています:-)
Damien_The_Unbeliever 2015年

3
@Damien_The_Unbelieverええ、それは間違いなく素晴らしい発見でした、私はすでにあなたの答えに賛成しました:P
ルアン2015年

1
構造体は、コードが非同期で実行されない場合、たとえばデータがすでにバッファーにある場合に非常に役立ちます。
イアンリングローズ2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.