C# Parallel.For + Native C での画像処理で、C#の約50倍の高速化


この投稿を2012年1月20日になくなられた マイクロソフトの偉大なエバンジェリスト 川西 裕幸さんに 捧げる。

約30年前に、東北大学の大型計算機センターに NEC ACOS 1000 という当時最高速の大型計算機が導入され、修論でだいぶお世話になりました。一つのビルがこの1つのコンピュータにささげられており、さながらバベルの塔のコンピューターのような畏敬の念をいだきながら、使ったものでした。
http://museum.ipsj.or.jp/computer/main/0053.html

時は流れ、今使っているPCは、メモリも、クロックも、ストレージも、通信も、おまけにSIMD プロセッサーに、MIMD マルチコアプロセッサ、高解像度ディスプレイまであり、当時の世界最高速の大型計算機の性能を軽く数ケタ凌駕している。恐ろしい性能を目の前に、ようやくi7 860 4コアのCPUを全力で 0.1秒だけ振り切らせることができた。それでも、SSEは全く使い余している。

30年後のテクノロジーに思いをはせながら、明日を夢見る。


image_thumb_thumb_thumb_thumb

前回、C# + Native で画像のラプラシアンフィルター実行してみたが、次に C# Parallel + Native C での実行と比較してみた。

ちょっと大きめの画像での実行結果は次の表のとおり。

番号 説明 C# (秒) C++/(秒) C# + Native C(秒) C# Parallel.For + Native C(秒)
  処理ロジック=> 素直にフィルターを実装 素直にフィルターを実装 素直にフィルターを実装 フィルターの3重for ループを展開
整数演算に変更
1 Debug 8.1 4.3 2.9 0.2
2 Release 4.8(コードの最適化 Off) 3.7: 最適化無効 (/0d) 2.1: 最適化無効 (/0d)
3 Release 4.5 (コードの最適化 On) 2.7 : 最大限の最適化 (/Ox) 0.5: 最大限の最適化 (/Ox) + SSE2 0.1秒
最大限の最適化 (/Ox) + SSE2

並列化による高速化は、アムダールの法則に従い、問題に内在する並列度に依存する。このような並列度の高い画像処理などでは並列化による高速化に向いており、C# で素直に実装(デフォルトのコンパイラスイッチ)したときが4.8秒に比べて、C# Parallel.For + Native Cで約50倍の高速化ができた。

実際のNative C のアセンブラ出力を確認してみると、次のようにSSE命令までは展開されていなかった。これは、フィルターの3重forループを展開し、さらに整数加減算+シフト演算へ変換しているため、SSE命令を使うより、単純なアセンブラの実行のほうが十分高速であるからと思われる。実際、手書きで SSE Intrinsics で積和演算を書いて実行してみたが、コンパイラーの最適化よりも、若干遅い結果となってしまった。もう少し複雑な演算や、浮動小数点の積和が必要な処理であれば、SSEコードが生成され、50倍以上の高速化も十分可能ではないかと期待している。

アセンブラのコードの一部
    mov    DWORD PTR tv423[ebp], eax
    mov    eax, ebx
    push    edi
    lea    edi, DWORD PTR [ebx+ecx]
    add    ebx, esi
    mov    DWORD PTR tv463[ebp], ebx
    mov    ebx, esi
    imul    ebx, DWORD PTR _y$[ebp]
    sub    eax, esi
    mov    DWORD PTR tv441[ebp], edi
    add    edi, esi
    mov    DWORD PTR tv428[ebp], eax
    add    eax, ecx
    mov    DWORD PTR tv422[ebp], edx
    add    edx, esi
    lea    esi, DWORD PTR [edx+ebx]
    mov    DWORD PTR tv442[ebp], edi
    add    edi, ebx
    add    ebx, eax
    add    edi, ecx

アセンブラのコード出力は、プロジェクトプロパティから、[構成プロパティ]→[C/C++]→[出力ファイル]→[アセンブリの出力] を設定する。

image

今回は4Core 8Thread の i7 860 での Parallel.For を利用したが、さらなるメニーコア化の時代が到来しており、これらのリソースを有効に活用できるソフトウェアの重要性がより高まってくるだろう。そんな時に、生産性の良い C# と.NET Parallel に、 Native C、SIMD Intrinsics の組み合わせは、最強の武器になるだろう。

3件のフィードバック to “C# Parallel.For + Native C での画像処理で、C#の約50倍の高速化”

  1. ぴ区民 Says:

    お初にお目にかかります。私も上記のコードを自分で試してみて、Parallelクラスの凄さを実感いたしました。
    ところで質問なのですが、Parallel.For + Native Cの部分は、外部DLLからMarshalでアンマネージポインタを渡して実行しているのだと思っているのですが、アンマネージ領域の確保時間やマネージへのコピー時間も入れての結果なのでしょうか?
    私の方で実験したところ、アンマネージ領域とマネージ領域間のコピーコストがかなり高く、ロジック最適化(C#)+Parallel.Forにしたものと結果に差がほぼ出ませんでした。
    これではコピーコストのせいでNaticeC DLLを使用した場合と結果が大して変わらない場合は、使用するメリットがあまりないのではと思っております。
    私の実装に間違いがある可能性もありますが、この辺りの所見をお聞かせ願えればと思います。

    • uchukamen Says:

      はい、Marshalでポインターを渡しています。時間計測はC#側で計っているので、コピーする時間も含めた結果です。ご指摘の通り、マーシャリングとNativeの処理のバランスだと思います。Native での処理に時間がかかり、それがマーシャリングに比べて十分大きければ、効果はあると思います。一方、Nativeの処理が軽すぎると、マーシャリングのオーバヘッドが大きくなってしまいまい、効果が出ないと思います。

  2. ぴ区民 Says:

    返信ありがとうございます。
    なるほど、”フィルタ処理が軽すぎる場合”が頭からスッポリ抜け落ちておりました。
    そのあたり考慮すると、NativeとC#の住み分けができそうですね(全部Native DLLにまとめたいという欲求が出てしまうかもしれませんが・・・笑)

    色々試してみます。ありがとうございました。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中


%d人のブロガーが「いいね」をつけました。