この投稿を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年後のテクノロジーに思いをはせながら、明日を夢見る。
前回、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++]→[出力ファイル]→[アセンブリの出力] を設定する。
今回は4Core 8Thread の i7 860 での Parallel.For を利用したが、さらなるメニーコア化の時代が到来しており、これらのリソースを有効に活用できるソフトウェアの重要性がより高まってくるだろう。そんな時に、生産性の良い C# と.NET Parallel に、 Native C、SIMD Intrinsics の組み合わせは、最強の武器になるだろう。
2012年3月7日 11:58 |
お初にお目にかかります。私も上記のコードを自分で試してみて、Parallelクラスの凄さを実感いたしました。
ところで質問なのですが、Parallel.For + Native Cの部分は、外部DLLからMarshalでアンマネージポインタを渡して実行しているのだと思っているのですが、アンマネージ領域の確保時間やマネージへのコピー時間も入れての結果なのでしょうか?
私の方で実験したところ、アンマネージ領域とマネージ領域間のコピーコストがかなり高く、ロジック最適化(C#)+Parallel.Forにしたものと結果に差がほぼ出ませんでした。
これではコピーコストのせいでNaticeC DLLを使用した場合と結果が大して変わらない場合は、使用するメリットがあまりないのではと思っております。
私の実装に間違いがある可能性もありますが、この辺りの所見をお聞かせ願えればと思います。
2012年3月8日 23:17 |
はい、Marshalでポインターを渡しています。時間計測はC#側で計っているので、コピーする時間も含めた結果です。ご指摘の通り、マーシャリングとNativeの処理のバランスだと思います。Native での処理に時間がかかり、それがマーシャリングに比べて十分大きければ、効果はあると思います。一方、Nativeの処理が軽すぎると、マーシャリングのオーバヘッドが大きくなってしまいまい、効果が出ないと思います。
2012年3月9日 11:54 |
返信ありがとうございます。
なるほど、”フィルタ処理が軽すぎる場合”が頭からスッポリ抜け落ちておりました。
そのあたり考慮すると、NativeとC#の住み分けができそうですね(全部Native DLLにまとめたいという欲求が出てしまうかもしれませんが・・・笑)
色々試してみます。ありがとうございました。