非同期テクノロジは、アプリケーション全体のスループットを大幅に向上するが、それはタダではない。非同期機能は、同期の代替よりも遅いことがあり、また注意を払わないと大幅にメモリを圧迫することになる。MSDN MagazineのStephen Toub氏は、先日「非同期のパフォーマンス:AsyncとAwaitのコストを理解する」というタイトルの記事で、このトピックについて取り上げた。
マネージドコードがネイティブC++を超えて、最も優れている機能のひとつは、実行時の関数埋め込みである。CLRのJITコンパイラは、アセンブリをまたいで関数埋め込みができ、OOPプログラマが好む細かいメソッドのオーバーヘッドを大幅に削減することができる。残念ながら、非同期呼び出しの本質は、委任で、埋め込みができないことを意味する。さらに、非同期呼び出しの準備をするための複雑な定型コードが必要になる。Stephen氏の最初の提案は、「よく喋るのではなく、大きく考えよう」。COMとp/invoke境界をまたぐ必要があるとき、小さく大量にではなく、少量の大きな非同期呼び出しをするべきである。
非同期パターンには開発者が明示的にnewオペレーター使うことなく、メモリを確保するいくつもの方法が存在している。このチェックを怠ると、メモリを過剰に圧迫して、ガベージコレクタが回収するために予期しない遅延が発生するだろう。Streamのサブクラスからの戻り値について考えてみよう。
public override async Task<int> ReadAsync(…)
return this.Read(…)
Readメソッドによって返されたintegerをラップして暗黙的に作られたTaskオブジェクトが示されていない。Stephen氏の記事の中では、最後のTask<int>をキャッシュした上で、それを再利用して、メモリのオーバーヘッドを削減する方法を紹介している。
他の想定外のオブジェクトが確保されて、保持される理由は、クロージャーの利用である。C#とVBのクロージャーは、メソッドの匿名と非同期関数を含む、匿名クラスとして実装される。これらの関数に必要なローカル変数は、匿名クラスに「クローズオーバー」または、「リフト」と言われる。このクラスのインスタンスは、親メソッドが呼ばれるたびに作成される。
しかし、余計なメモリ確保の問題はこれだけではない。通常のローカル変数によるのオブジェクト参照は強く主張しており、現在の関数で使わないことが明らかになるとすぐにGCが回収することができる。匿名関数によって使われる“ローカル変数”は、実際には非同期クラスのフィールド変数であり、呼び出しの間、保持されなくてはならない。もし、数秒かかる場合、一般的な非同期呼び出しではなく、匿名クラスは予期せずに、より高価な第1または2世代に昇格してしまうかもしれない。Stephen氏は、これが問題になる場合、不要になった時点で明示的にローカル変数にnullをセットすることを推奨している。
Stephen氏の3番目の問題として、コンテキストの概念、特にsynchronization contextとexecution contextについて議論している。 彼の記事では、どうやってライブラリコードがConfigureAwaitメソッドを使って、意図的にsynchronization contextを無視し、execution contextがキャプチャが必要なことを避けることによって、パフォーマンスを向上することができるかを示している。