Chiharu の日記

絵描き C/C++ プログラマーの日記です。

Parallel force - パラレル・フォース 〜ちょっと高速化。その 3 (SSE2)

結局、SIMD 対応しました。描画処理全般で SSE2 を使ってみました。先週から、いろいろ書き換えて効果が出てきたので、サンプルを公開します。実行ファイルの内訳は下記のとおりです。Intel Core 2 / i3 / i5 / i7 シリーズを使用している場合は、特に SSE2 の効果が体感できると思います。Intel Core 2 から、SSE2 はリリース当初の倍の速度が出るようですので。

  • pforce_1.exe … 単一スレッド処理。(15fps)
  • pforce.exe … 複数スレッド処理。(62fps)
  • pforce_loop.exe … 複数スレッド処理、ループ展開。(70fps)
  • pforce_loop_alpha.exe … 複数スレッド処理、ループ展開、ブレンド処理最適化 (C++)。(81fps)
  • pforce_loop_sse2.exe … 複数スレッド処理、ループ展開、ブレンド処理最適化 (SSE2)。(125fps)

カッコ内の値は Intel Core i7 マシンでの計測結果です。前回 C++ での最適化ルーチンで 81fps だったものが、今回 SSE2 最適化ルーチンにて 125fps 出るようになりました。SIMD 侮れませんね。かなり速いです。SSE2 では 4 画素ずつ同時処理しているのですが、そう考えると、逆に 1 画素ずつ処理している C++ のコードも健闘しているのだな、とちょっと感心しています。SSE2 の方は処理の過程でビット深度が変動するため (ロード時:8x16, 乗算/シフト時:16x8, ストア時:8x16)、そのたびにパック & アンパック命令を入れており、必ずしも 4 倍の速度が出せるわけではなさそうです。
今回は組み込み関数でお手軽に SSE2 対応しました。かりっかりにチューニングするにはインライン アセンブラでも使うのがよいのでしょうが、VC が確か x64 にてインライン アセンブラ非対応だったのを受けて、組み込み関数での実装でまとめました。
ただ、ここでひとつ問題が…。
SSE2 組み込み関数を使用した場合、XMM レジスタの扱いに制限が入ります。VC (icc/gcc も同様) では __m128i 等の型を使用することにより、XMM レジスタ相当の変数を宣言できるのですが、これは必ずしもレジスタ割付されるとは限りません。当該変数の宣言数がレジスタ数よりも多い場合や、変数の宣言から参照までコード上の距離が離れている場合等、コンパイラにより一時的に XMM レジスタの内容がスタックへストアされることがあります。この動作自体は int や long など、他の一般的な変数宣言と同じ扱いですので、驚くところではないのですが、XMM レジスタのロード元とストア先は、SSE2 の仕様により 16 バイト アライメントされている必要があります。main 関数からカウントして SSE2 利用箇所までが一貫した自作コードであれば、コンパイラがアライメントを制御してくれるので問題ないのですが、途中で他のライブラリ等のコールバックを挟んでしまうと、16 バイト アライメントが正常に機能しません。コールバック元のライブラリ関数でアライメント保証がされなくなるためです。(32bit/64bit アプリにおける最大の組み込みデータ長は通常 8 バイトですので)
パラレル・フォースでは、ワーカー スレッド内で SSE2 最適化された描画処理が走るため、_beginthread API でスレッド プロシージャを呼び出した時点で、アライメント保証がなくなってしまいます。先ほどまで、この挙動に手こずってしまい、原因判明するまで、謎の強制終了処理に悩まされていました。結局、XMM レジスタ退避が発生しないよう、XMM 自動変数の宣言数を減らして、宣言から参照までの距離を極力短くすることで対応しました。
レジスタ退避がなくなった分、速度貢献はしていると思うのですが、対症療法につき、抜本的な解決ではありません。んー。任意のタイミングで簡単にスタックアドレスをずらす方法ってあるんでしょうか。
※現在はサンプルを公開していません。