連載:C# 4入門

第2回 タスク並列ライブラリ

株式会社ピーデー 川俣 晶
2010/08/20
Page1 Page2 Page3

パラレルへの発想の転換

 ここに1つのモノクロ(2値)のPNG画像(1024×1024)があるとしよう。モノクロなので、白と黒しかあり得ない(実際は白と黒だけの8bit PNG形式画像ファイルを使用)。果たして、黒が何ピクセルなのだろうか。それを数えるプログラムを作成するとしたら、どうなるだろうか。1分割(従来型)から2〜4分割(パラレル使用)まで、それぞれの実行速度を調べてみよう(コンパイルにはSystem.Drawing.dllへの参照を追加する必要あり)。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;

class Program
{
  public static IEnumerable<Color> Pixels(int from, int to)
  {
    var bm = new Bitmap(@"s:\sample001.png");
    for (int y = from; y < to; y++)
    {
      for (int x = 0; x < bm.Width; x++)
      {
        yield return bm.GetPixel(x, y);
      }
    }
  }

  private static bool isBlack(Color c)
  {
    return c.B == 0 && c.G == 0 && c.R == 0;
  }

  private static void count(int fromInclusive, int toExclusive, ref int count, object lockTarget)
  {
    var q = from n in Pixels(fromInclusive, toExclusive)
            where isBlack(n)
            select n;

    int r = q.Count();
    lock (lockTarget) { count += r; }
  }

  static void Main(string[] args)
  {
    Object thisLock = new Object();

    DateTime start1 = DateTime.Now;
    int count1 = 0;
    count(0, 1024, ref count1, thisLock);
    Console.WriteLine(DateTime.Now - start1);
    Console.WriteLine("{0} Black Pixels by single task", count1);

    DateTime start2 = DateTime.Now;
    int count2 = 0;
    Parallel.Invoke(() => count(0, 512, ref count2, thisLock),
      () =>count(512, 1024, ref count2, thisLock));
    Console.WriteLine(DateTime.Now - start2);
    Console.WriteLine("{0} Black Pixels by 2 task", count2);

    DateTime start3 = DateTime.Now;
    int count3 = 0;
    Parallel.Invoke(() =>count(0, 342, ref count3, thisLock),
      () => count(342, 682, ref count3, thisLock),
      () => count(682, 1024, ref count3, thisLock));
    Console.WriteLine(DateTime.Now - start3);
    Console.WriteLine("{0} Black Pixels by 3 task", count3);

    DateTime start4 = DateTime.Now;
    int count4 = 0;
    Parallel.Invoke(() =>count(0, 256, ref count4, thisLock),
      () => count(256, 512, ref count4, thisLock),
      () => count(512, 768, ref count4, thisLock),
      () => count(768, 1024, ref count4, thisLock));
    Console.WriteLine(DateTime.Now - start4);
    Console.WriteLine("{0} Black Pixels by 4 task", count4);
  }
}
リスト7

 このプログラムには一見冗長に見えるが、実は必要とされて、そう書いているコードが多い。まず、パラレルに分割された処理1つ1つで画像ファイルを読み込んでいる。4分割なら4回同じファイルを読み込んでいる。これは一見無駄に見えるが、Bitmapクラスはスレッド・セーフではないので、別スレッドからのアクセスを受け付けない。そのため、無駄を承知で、処理するスレッド内で同じファイルを読み込んでいる。

 lockステートメントを使用している点にも注意しよう。実は、この程度のプログラムであっても、アクセスが競合してデータが壊れる懸念があるのだ。

 また、以下のリスト7内のコード部分では、変数rを除去できるように思えるかもしれない。

int r = q.Count();
lock (lockTarget) { count += r; }

 以下のように書いてしまうと、変数rを除去できる。

lock (lockTarget) { count += q.Count(); }

 しかし、ピクセルを探す処理は、実際にはCountメソッド内で実行されるのである。そのため、このような書き換えを行うと、その処理本体が排他的にロックされてしまい、処理が全体として遅くなってしまうのである(CPUの複数コアが有効活用されない)

 もう1つ注目すべき点は、分割数が物理的に存在するコア数を超えると、かえって遅くなる場合があることだ。以下は2コアCPUで実行した例である。

00:00:07.9630000
16470 Black Pixels by single task
00:00:03.0170000
16470 Black Pixels by 2 task
00:00:03.0870000
16470 Black Pixels by 3 task
00:00:03.0850000
16470 Black Pixels by 4 task
リスト7の実行結果(1)
2コアCPUでの実行例。結果は、2分割 < 3分割 ≒ 4分割 << 分割なし。

 つまり、同じコアを複数のタスクで分割して利用してしまうと、コンテキスト・スイッチのオーバーヘッドだけ重くなるということだろう。だから、2コアでもHT技術により4個あるように見えるCore i5では結果が変わってくる。

00:00:02.8451627
16470 Black Pixels by single task
00:00:01.4070805
16470 Black Pixels by 2 task
00:00:01.1200640
16470 Black Pixels by 3 task
00:00:01.0490600
16470 Black Pixels by 4 task
リスト7の実行結果(2)
Core i5での実行例。結果は4分割 < 3分割 < 2分割 << 分割なし。

 この場合は、素直に4タスクまでタスク分割が増えると、より速くなるという傾向が見られる。しかし、本物のコアは2個しかないので、スピードアップの効果は2タスクまでは劇的だが、それを超えると、それほど大きくはない(それでも速くはなっていて無駄ではないが)。

まとめ

 今回のまとめ。

  • メニー・コア時代はすでに始まっている
  • ソフトウェア開発のメニー・コア時代はVisual Studio 2010/C# 4/.NET Framework 4から
  • 並列実行はParallel.Invokeクラスで簡単
  • foreach文はParallel.ForEachメソッドに
  • for文はParallel.Forメソッドに
  • しかし、すべてのループが単純にパラレルに置き換えられるわけではない
  • ちょっとした書き方の差で大幅に性能に差が出ることも

 今回筆者が驚いた冒頭のノートPCはSpursEngineという独自プロセッサのコアを含むので、これらをC# 4で有効活用することは難しい。しかし、この先、コアが増えることがあっても、減ることは考えにくい。メインCPUのコア数もこの先増えていくことが予想される。コア数の活用結果は、2コア4スレッドのCore i5クラスでは、まだ見えにくいのかもしれないが、このクラスのPCのメインCPUが本当に4コア以上になる時代も遠くないだろう。

 そうなったとき、パラレルの知識を活用したプログラムと、従来型のプログラムは劇的な性能差を生むかもしれない。だから、後生大事に古いソース・コードを抱えて、いつまでも待っていくよりも、新しいパワーを解放するために新しいコードを書くことが重要である。

 性能を発揮しやすいアルゴリズムも変わるであろうから、古いコードを手直しして対処できる可能性もそれほど大きくはない。それ故に、他人が書いた古いソース・コードを入手するために努力するよりも、自分で新しい時代の新しいコードを書いていこう。それが利用者の利便性を高め、感謝される早道である。End of Article

 

 INDEX
  C# 4入門
  第2回 タスク並列ライブラリ
    1.単純に2コアを使う
    2.本当にコアを活用しているの?/foreach文をパラレルに/for文をパラレルに
  3.パラレルへの発想の転換/まとめ
 
インデックス・ページヘ  「C# 4入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間