C++, C++ AMP, CUDA, PPL

[C++][CUDA][PPL][AMP]単純な二値化処理を4通りの並列化手法で比較してみる

画像処理の分野において、各ピクセル(空間の場合は各ボクセル)の値を0か1かのどちらかの値に分ける二値化処理が頻繁に行われます。
何を以て0または1とするかがポイントなわけですが、ここでは超簡単に、閾値(threshold)未満だったら1、以上だったら0になることを考えてみます。

例えば全部で8ピクセルの画像で、各ピクセルの値がint型で与えられているときに閾値40であれば以下のようになります。

条件式が1回しか出てこないような処理でも、ピクセル(ボクセル)の総数が大きくなれば並列化の意味があるのかどうかを検証するために、
縦256, 横256, 高さ256:合計16,777,216ボクセルのボリューム画像を仮定して、

1. CPUシングルスレッド
2. 並列パターン ライブラリ (PPL)によるfor文のマルチスレッドCPU処理
3. C++ AMP (C++ Accelerated Massive Parallelism)によるGPGPU並列処理
-3-1. concurrency::array_viewによるもの
-3-2. concurrency::arrayによるもの
4. CUDAによるGPGPU並列処理

の合計5通りで比較してみました。

以下、コード一式です。コードの下に実行結果を掲載します。
もちろんPCの環境によって数字は変わるかと思いますので、あくまでも参考値ではあります。
コードはどうでも良いから結果だけ見たいという方は、すっ飛ばして一番下のほうを見てください。

乱数の生成方法は本の虫: C++0xの新しい乱数ライブラリ、random
C++ AMPのアクセラレータの取得・決定方法はC++ AMPによるGPGPU入門 – Qiita

を参考にさせて頂きました。
.cuファイル内でC++ AMPをインクルードしようするとコンパイル時にエラーが出たため、.cuファイルと対応する.hファイル、そしてmain関数を含んだ.cppファイルの3つを作りました。
やたらと長いですが、本質はどれもハイライトで表示した1行だけで、その他は並列化のための下準備であったり、どの手法でも同じ結果になっているかのチェックだったりです。

◆実行結果
——————————–
Single CPU: 10 msec.
——————————–
PPL: 8 msec.
OK
——————————–
C++ AMP: Initialize: 49 msec.
array_view:
Make array_view: 0.036 msec.
GPU: 13 msec.
GPU -> CPU: 12 msec.
OK
—————-
array:
Make array: 9 msec.
GPU: 0.142 msec.
GPU -> CPU: 28 msec.
OK
——————————–
CUDA:
Initialize: 170 msec.
GPU Malloc: 99 msec.
CPU -> GPU: 9 msec.
GPU: 0.622 msec.
GPU -> CPU: 9 msec.
OK
——————————–

なんということでしょう。これくらいの単純な処理の場合、1000万以上の要素があってもシングルCPU処理とマルチスレッドCPU処理でそこまで処理時間が変わらず、GPGPUによる並列化はCPU, GPU間のメモリ移動がどうしようもなくボトルネックになっています
また、GPGPUの場合はC++ AMPにしろCUDAにしろ、初期化の時間がそこそこありCUDAに至ってはGPU上のメモリ確保が恐ろしく遅い、ということがわかりました。

今回のような極めて単純な処理の場合、処理をした結果をCPU側で用い、且つ、その処理は一度しか行われないような場合には、わざわざ悪戦苦闘してCPUマルチスレッド化やGPGPU化をしたところでほとんど変わらないか、大幅にパフォーマンスが落ちてしまい、全く意味がないという結論になりました。

C++ AMPのarray_viewとarrayとで計算時間にここまで差が出る理由は良くわかりませんでした…。array_viewの場合はカーネル関数実行時に初めてCPU -> GPUへのメモリ転送が行われるとのことなので、それを考慮すると

array_view:
GPU: 13 msec.

はCPU -> GPUとGPU上での計算を合わせた時間が13 msecと考えるのが良いのだと思いますが、arrayの場合にGPU -> CPUへの転送に28 msecもかかるのは驚きでした。

今回のように、ピクセル(ボクセル)の値が不変の場合には、最初にCPU -> GPUにデータを転送してしまえばそのデータを更新する必要はありませんおで、もし閾値が色々変わることで繰り返しこの二値化処理を行うのであれば、
CUDAの場合は

CUDA:
GPU: 0.622 msec.
GPU -> CPU: 9 msec.

の合計時間、つまり約9.6 msecとして評価すれば良いのですが、GPU上での計算は0.6 msecと爆速なのにもかかわらず、GPU -> CPUへのデータ転送に9 msecもかかってしまい、シングルCPU処理とほぼ変わらないというとても残念な結果になってしまいました。

並列処理出来るからと言って、いつ何時でもCPUマルチスレッド化やGPGPU化をしたほうが速いとも限りませんね。もっと込み入った計算をするときなど、メモリ間のデータ転送時間が気にならないくらいの処理をするときに初めて並列処理の強みが活かされそうです。


※本記事内容は、国立研究開発法人 日本医療研究開発機構(AMED)の平成29年度 「未来医療を実現する医療機器・システム研究開発事業『術中の迅速な判断・決定を支援するための診断支援機器・システム開発』」採択課題である「術前と術中をつなぐスマート手術ガイドソフトウェアの開発」(代表機関名:東京大学、研究開発代表者名:齊藤延人)に、東京大学大学院情報理工学系研究科の学術支援専門職員として参画している瀬尾拡史が、研究開発として行っているものやその成果を一部含んでいます。

コメントを残す

メールアドレスが公開されることはありません。