Microsoftは、.NET 4.5で並列アプリケーション(特にTask Parallel Libraryに)のパフォーマンスを向上に取り組んでいる。これは、あなたが期待できることのプレビューである。:
Task, Task<TResult>
.NETの並列プログラミングAPIのコアは、Taskオブジェクトである。このように重要なクラスで、Microsoftは可能な限り小さくできるように英断を行った。Taskのほとんどのプロパティはクラス自身ではなく、ContingentPropertiesと呼ばれる二次オブジェクトに保持している。この二次オブジェクトは、必要に応じて作成されるため、最も一般的なシナリオでは、メモリ使用量を減らすことができる。
.NET 4.0のリリース時点での最も一般的なシナリオは、Parallel.ForEachとパラレルLINQに見られるようなfork-joinスタイルのプログラミングであった。.NET 4.5では、asyncによる継続スタイルのプログラミングが最前面になる。Microsoftは、Taskと他のフィールドをContingentPropertiesに含めたContinuationObjectに移行することで、支配的なスタイルになることを確信している。結果として、より早い継続性とより小さいTaskオブジェクトができあがる。
Task<TResult>は、望まない待機を削減する。もともとは4つのフィールドだったが、Joseph E. Hoag氏は次のように説明する:
何度かのスマートな再構築によって、実際にはm_resultフィールドのみが必要であることがわかりました。すでにベースのTaskクラスに存在しているフィールドは、m_valueSelectorとm_futureStateは廃止される可能性があり、m_resultWasSetによって保存された情報は、ベースタイプの前述の状態フラグで代用することができます。
最終的にTask<Int32>を作成する時間が49~55%削減でき、52%のサイズを削減することができた。
Task.WaitAll、Task.WaitAny
1億のタスクが同時に待機していることを想像して欲しい。x64マシン上では、タスク自身のサイズを超えて、12,000,000バイトのオーバーヘッドが発生する。.NET 4.5では、オーバーヘッドはわずか64バイトに削減されている。WaitAnyも同様に23,200,000バイトのオーバーヘッドを152バイトに削減している。
このドラマチックな変更は、カーネル同期プリミティブの使われ方によるものであった。以前のバージョンではプリミティブは、タスクごとに必要であった。これが待機操作ごとに1つに削減され、タスクの数には関係なくなった。
ConcurrentDictionary
.NETでは、参照型と小さな値型のみがアトミックに確保できる。Guidが必要になるような大きな値型は、アトミックに読み書きをすることができない。.NET 4.0では、ConcurrentDictionaryで使われているノードオブジェクトは、キーと値が変更される度に再構築されていた。.NET 4.5では、値をアトミックに書き込めない場合、新しいノードのみが作成される。
その他の変更としては、動的に新しいロックを作成する機能である。Igor Ostrovsky氏は、次のように記述している。
経験上、スループットを最大にするために、大量のロックを要求することがある。その一方で、特に少量のアイテムのみを保持している場合、多すぎるロックオブジェクトを確保したくはない。
パフォーマンスの向上、メモリ確保の削減
Joseph氏は以下のように記述する。
> 私たちのベンチマークの結果からわかるように、テストでメモリの確保される量と、テストが完了するまでの時間には直接的な相互関係が存在している。個々で見た場合、メモリの確保量はそれほど高価ではない。辛いのは、メモリシステムがときどき未使用のメモリをクリーンアップするときで、これはメモリの確保量に比例して発生する。そのため、さらなるメモリを確保した場合、より高い頻度でメモリがガベージコレクトされ、あなたのコードのパフォーマンスが劣化することになる。
メモリの使用量を削減するひとつの方法は、クロージャの利用を避けることである。匿名関数内部のローカル変数としてキャプチャするよりも、“状態オブジェクト”として、Taskのコンストラクタに情報を渡すことができる。 .NET 4.5から、Task.ContinueWithは、状態オブジェクトもサポートしている。
その他のメモリ使用量を削減するテクニックは、一般的に使われるタスクをキャッシュすることである。たとえば、配列を受け取り、Task<int>を返す関数を検討してみよう。空の配列の結果は常に同じになるため、空の配列を表すTaskをキャッシュする意味があるだろう。
次のヒントは、不必要に“膨張する”タスクを避けることである。なにかをきっかけにContingentPropertiesオブジェクトが作成されるとき、Taskは膨張する。最も一般的な理由は:
- TaskがCancellationTokenと共に作成された
- TaskがデフォルトじゃないExecutionContextから作成された
- Taskが親Taskとして“構造化された並列性”に参加している
- TaskがFaultedステータスで終了した
- Taskが((IAsyncResult)Task).AsyncWaitHandle.Wait()を使って待機している
タスクの膨張が必ずしも悪くないことに注意して欲しい。むしろ、二度と使われないCancellationTokenにパスするような、不必要なことをしていないことを意識するためのものである。