AsyncとAwaitの代価を使う

3571 ワード

非同期テクノロジーは、アプリケーションの総スループットを大幅に向上させることができますが、これは無償ではありません.非同期関数は、同期の代替案よりもやや遅いことが多く、気にしないで使用するとメモリ圧力がかなり増加します.Stephen Toubは最近MSDN誌の「非同期性能:AsyncとAwaitの代価を理解する」と題した文章でこのテーマを議論した.
ホストコードの最も顕著な利点の1つは、ネイティブC++コードに対して実行時インライン関数(inline function)[1]の能力である.CLRのJITコンパイラは、プログラムセットにわたって関数をインラインすることもでき、これにより、微細度メソッド(OOPプログラマがこのようなメソッドを好む)を呼び出すオーバーヘッドを大幅に低減することができる.残念なことに、非同期呼び出しの本質は、インライン委任(delegates cannot be inlined)ができないことを意味する.さらに、非同期呼び出しを確立する際には、多くのテンプレートコードも含まれます.これにより、Stephenの第1の提案は、「細粒度(Think Chunky,Not Chatty)ではなく、粗粒度を考慮する」ことになった[2].COMまたはp/invokeの境界を越えているように、多くの小型非同期呼び出しよりも少数の大型非同期呼び出しが好きになるはずです.
非同期モードでは、new演算子を開発者が明示的に使用する必要がなく、メモリをさまざまな方法で割り当てることができます.いずれにしても、これらのメモリ割り当て法は、メモリ圧力が大きすぎる可能性があり、ゴミ回収器のフォローアップが試みられるため、不要な遅延をもたらす可能性があります.Streamサブクラスからのこの署名とその戻り文を考慮します.

public override async Task<int> ReadAsync(…)
return this.Read(…)

ここでは、Readメソッドから返される整数値をパッケージングするための暗黙的に作成されたTaskオブジェクトは示されていません.Stephenの記事では、最近のTaskオブジェクトをキャッシュし、再利用することでメモリオーバーヘッドを低減する方法を示しています.
予期せぬオブジェクトの割り当てと保持をもたらすもう1つの理由は、閉パッケージ(closures)の使用です.C#とVBの閉パケットは匿名クラスによって実現され,匿名クラスには匿名メソッドが含まれ,メソッドには非同期関数が宣言されている.匿名関数に必要なローカル変数は、匿名クラスに「クローズド」(closed over)または「リフト」(lifted)されると言われています.匿名クラスの親メソッドを呼び出すたびに、クラスインスタンスを作成する必要があります.
問題はこのままではなく、追加のメモリ割り当てをさらに悪化させる可能性があります.通常、ローカル変数が参照するオブジェクトはホットリクエストされ、ゴミ回収器(GC)は、ローカル変数が現在の関数で使用されなくなることを明確にすると回収します.非同期関数で使用されるローカル変数は、実際には匿名クラスのフィールドであるため、呼び出し中に保持する必要があります.このプロセスに数秒かかる場合、これは非同期呼び出しによく見られるが、匿名クラスは、何気なくゴミ回収器の中でより高価な1世代または2世代のオブジェクトに昇進する可能性がある[3].これが問題になった場合、Step henは、ローカル変数が不要になったら、明示的に空の参照に設定することをお勧めします.
Step henで議論した3つ目の問題は,コンテキストの概念,特に同期コンテキスト(synchronization context)と実行コンテキスト(execution context)である.彼は、ConfigureAwaitメソッドを使用して同期コンテキストを故意に無視し、実行コンテキストでキャプチャしなければならないことを回避することによって、ライブラリコードがパフォーマンスの向上を得る方法を示した.
訳注
[1]インライン関数(inline function)は、異なるプログラミング言語において、インライン関数(inline function)とは、エディタにインライン展開(inline expansion)を実行するように要求された関数を指す.すなわち、プログラマは、コードを生成して定義された場所から関数を呼び出すのではなく、コンパイラに関数を呼び出す場所ごとに完全な関数体を挿入するように要求した.C 99またはC++を使用して、次のようなインライン関数を記述できます.

inline int max(int a, int b)
{
  return (a > b) ? a : b;
}

次に、呼び出し文は次のようになります.

a = max(x, y);

この文はコンパイル後、より直接的な計算に変換される可能性があります.

a = (x > y) ? x : y;

詳細はInline functionを参照してください.
 
[2]細粒度(Think Chunky,Not Chatty)ではなく粗粒度を考慮すると、ChunkyとChattyの争いはこれまで「サービス協定設計」(service contract design)で多く見られてきた.くどくど言うサービス(Chatty Service)は、簡略化された情報を返し、より細かい操作を使用する傾向にある.小太りのサービス(Chunky Service)は、複雑な階層情報を返し、太さの操作を使用する傾向があります.言い換えれば、両者の違いは、同じ情報を返す場合、小言のサービスは、太ったサービスに比べてより多くの呼び出しを必要とするが、実際に必要とされる適切な情報を返す柔軟性を増加させることである.詳しくはWCFサービスcontract designを参照してください.
[3]1世代または2世代の対象であり、「世代」はゴミ回収器で用いられる概念である.ごみ回収器といえば「管理スタックの簡略化モデル」と言わざるを得ないが、このモデルのルールは以下の通りである.
  • ゴミ回収可能なすべてのオブジェクトは、連続したアドレス空間範囲(管理スタック)内に割り当てられる.
  • スタックは世代(generation)に分けられ、スタックの一部を検索するだけで多くのゴミを除去することができます.
  • 世代の対象はほぼ同世代であった.
  • 世代の番号が高いほど、スタックのこの領域に含まれるオブジェクトが古いことを示します.これらのオブジェクトは安定している可能性があります.最も古いオブジェクトは最も低いアドレスにあり、新しいオブジェクトは増加したアドレスに作成されます.
  • 新しいオブジェクトの割り当てポインタには、メモリの使用済み(割り当て済み)メモリ領域と使用不可(使用可能)メモリ領域の境界がマークされます.
  • は、デッドオブジェクトを削除し、生オブジェクトをスタックの低アドレスの末尾に移動することによって、スタックを周期的に圧縮する.これにより、新しいオブジェクトを作成するグラフの下部にある未使用領域が拡張されます.
  • オブジェクトのメモリ内の順序は、配置を容易にするために作成される順序のままです.
  • スタック内では、オブジェクト間に隙間はありません.
  • には、コミットされた空き領域があるだけです.必要に応じて、オペレーティングシステムは「保存された」アドレス範囲からより多くのメモリを割り当てます.

  • 詳細については、「ゴミ回収機の基礎とパフォーマンスのヒント」を参照してください.
     
    The Cost of Async and Await