Partitioner クラス

Last Updated 2011/09/21


Parallel.ForEach メソッドの中に Partitioner オブジェクトを指定する構文があります。このページではこのクラスについて解説します。


Parallel.ForEach メソッドを使ってシーケンス(コレクション、リスト、配列)を処理するとき、一つ一つの項目の処理時間は短いが、項目数がすごく多い場合、処理を並列化するためのオーバーヘッド(処理に付帯する手続き)が相対的に大きくなり、並列処理のメリットを得ることができなくなる可能性があります。

そこで、シーケンス自体を複数に分割し、それぞれのブロックを複数のコアに割り当て、データの処理はシーケンシャルに実行することで処理の高速化を図ろうというものです。分割したブロックをパーティションと呼びます。

Note .NET Framework SDK にこう書いてあるわけではありませんが、いろいろな情報をかき集めて考えるとこれでいいだろうと考えています。

さて、パーティションを使用すると、実際に処理の高速化が図れるのかどうかをチェックしましょう。

private void button1_Click(object sender, EventArgs e)
{
  int N = 10000000; // この値を小さくすると、パーティションを使う効果が小さくなる
  int rangeSize = N / Environment.ProcessorCount;  // これがベストかどうかは分からないがコア数を基準とすると分かりやすい

  int[] collection = Enumerable.Range(0, N).ToArray();
  int total = 0;
  object lockObject = new object();

  var partitioner = Partitioner.Create(0, N, rangeSize);

  var watch = Stopwatch.StartNew();

  //シーケンシャル処理
  for (int i = 0; i < collection.Length; ++i)
  {
    total += collection[i] * 2;
  }

  watch.Stop();
  textBox1.Text = String.Format("シーケンシャル処理の結果:{0}, 時間:{1} ミリ秒\r\n",
	    total, watch.ElapsedMilliseconds);

  // 並列処理
  total = 0;
  watch.Restart();

  Parallel.ForEach(
    partitioner,
    () => 0,

    (range, loopState, initialValue) =>
    {
      int partialSum = initialValue;

      for (int i = range.Item1; i < range.Item2; ++i)
      {
        partialSum += collection[i] * 2;
      }

      return partialSum;
    },

    (localPartialSum) =>
    {
      lock (lockObject)
      {
        total += localPartialSum;
      }
    });

  watch.Stop();
  textBox1.Text += String.Format("並列処理の結果:{0}, 時間:{1} ミリ秒\r\n",
	    total, watch.ElapsedMilliseconds);
}

実行結果は示しません。というのは、実行環境によって結果も異なるだろうと考えたからです。ポイントは、N と rangeSize です。私の環境(コア数は 4)では N = 4000000 ぐらいのところでシーケンシャル処理と並列処理との処理速度の分岐点になりました。みなさんの環境の中でいろいろな数値をあてはめてテストしてみてください。

−以上−