web-dev-qa-db-ja.com

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

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

編集:

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

86
gregkalapos

私はこれについて何の予知も持っていませんでしたが、最近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を使用することにはある程度の魅力はありますが、 Edit and Continueasyncメソッド内で機能することの大きな勝利が明らかにより良いオプションとして選択されました。

110

このようなことについて明確な答えを出すのは難しいですが(コンパイラチームの誰かが立ち寄らない限り:))、考慮できる点がいくつかあります。

構造体のパフォーマンス「ボーナス」は常にトレードオフです。基本的に、次のものが得られます。

  • 値のセマンティクス
  • 可能なスタック(登録することもできますか?)の割り当て
  • 間接化の回避

待機中の場合、これはどういう意味ですか?まあ、実際には...何も。ステートマシンがスタック上にあるのは非常に短い期間だけです-awaitreturnを効果的に実行するため、メソッドスタックは終了します。ステートマシンはどこかに保存する必要があり、その「どこか」は間違いなくヒープ上にあります。スタックの寿命は非同期コードにうまく適合しません:)

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

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

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

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

最後に、実際のパフォーマンスの違いが本当に見られるのはベンチマークだけだと思います。そして、控えめに言っても、ベンチマークの最適化は馬鹿げたアイデアです...

3
Luaan