ひきぷろのプログラミング日記

プログラミングの日記です。

(C#) ピクセル操作を並列化する

前の記事で、ピクセル操作を簡単に書くために PixelManipulator というクラスを作りました。

hikipuro.hatenadiary.jp

実行効率を無視して作ってしまったため、速度が遅いです。お手軽に高速化するために、スレッドを使って並列化する処理を追加してみました。
まずは、次のメソッドを PixelManipulator に増やします。

// ピクセルを巡回する処理を並列化する
public void EachPixelConcurrent(Action<int, int> action) {
	// スレッドのリスト
	List<Thread> threads = new List<Thread>();

	// CPU のコアの数
	int count = Environment.ProcessorCount;
	for (int i = 0; i < count; i++) {
		// 開始パラメータ付きのスレッドを作成する
		// ラインごとに、処理する CPU コアを分けているつもり
		// (スレッドの割り振りが期待通りに分散されていれば、、)
		Thread thread = new Thread(new ParameterizedThreadStart((sy) => {
			for (int y = (int)sy; y < height; y += count) {
				for (int x = 0; x < width; x++) {
					action(x, y);
				}
			}
		}));

		// スレッドを開始する
		thread.Start(i);
		threads.Add(thread);
	}

	// 全てのスレッドの終了を待つ
	foreach (Thread thread in threads) {
		thread.Join();
	}
}

後は、フィルタの処理に書かれている EachPixel() の部分を EachPixelConcurrent() に置き換えるだけで並列化処理版に置き換わります。4 コアの環境で実行してみたところ、半分くらいの時間で処理が終わるようになりました。 4 倍速にはならないんですね。。

最近の PC は CPU コアが増える傾向にあるので、並列化処理を試してみるというのは良い案な気がします。今回の場合は、入力側のビットマップ画像が変更されない前提なので簡単に並列化処理を追加できましたが、場合によってはマルチスレッド特有の難しさに直面するかもしれません。上のコードのように簡単に書けるケースであれば、まずは試してみるのが良さそうな気がします。マルチスレッドに慣れていなければ、この処理を並列化するのは筋が悪そうだとか、予想するのも難しそうですよね。

もっと高速化しようと思ったら、 GPU に処理を書くのが良さそうな気がします。 GPU ははるかに並列数が多いので、処理内容によっては、めちゃくちゃ早くなる場合もありそうです。ただ、 VisualStudio だとどうやって GPU の処理を書いたら良いのか分かりません。。 Unity だと、 Compute Shader が簡単に扱えるので、 GPU を使って処理を並列化すること自体は難しくなさそうです。 Unity を使った場合でも、画像処理をする必要はなくて、自由に処理を書くことができるようです。 GPU を使った時に難点があるとすると、実行できるコードの分量に上限があって、コンパクトなコードしか実装できなさそうということでしょうか。どれくらいの制限かは型番によっても違いそうだし、調べてみないと分からないですが、長いコードだと実行できないかもしれません。また、 C# でコードが書けないことと、使用できる関数に制限があることも GPU のプログラムを書くときにネックになるかもしれません。 GPU では、 C 言語に似たコードでプログラムを書く必要があります。使用できる関数に制限があるというのは、便利なライブラリが標準では用意されていないということです。数学の関数はある程度使えそうですが、乱数の生成すらできなかったような記憶があるので、いろんなサイトを検索したりして、地道に実装していく必要が出てきます。

追記

上で Thread を使って並列化していましたが、 .NET Framework 4 以降の環境でビルドする場合は、単純に Parallel.For() を使う方が簡単に並列化できそうです。
EachPixel() の処理を Parallel.For() を使って並列化する場合は、次のように変更します。

// 変更前
public void EachPixel(Action<int, int> action) {
	for (int y = 0; y < height; y++) {
		for (int x = 0; x < width; x++) {
			action(x, y);
		}
	}
}

// 変更後
// (using System.Threading.Tasks; を追加する必要がある)
public void EachPixel(Action<int, int> action) {
	Parallel.For(0, height, (y) => {
		for (int x = 0; x < width; x++) {
			action(x, y);
		}
	});
}

ほんのわずかな書き換えで、並列化することができました。
Thread を使うよりも、こちらの方が効率が良いらしく、 4 コアの環境で、

  • Thread を使った場合: 1.5 倍速から 2 倍速くらい
  • Parallel.For() を使った場合: 2 倍速から 3 倍速くらい

というような違いがありました。
書くのも楽だし、速いし、こっちの方が色んな意味で良さそうですね。