Archives

21 April 2007 の記事

VisualStudio2005 は /arch:SSE2 オプションで SSE 命令を使って
コンパイルしてくれます。とはいえ積極的なベクトル化までは行われないし
SSE3/SSSE3 を含めて全部の SSE 機能が使われるわけではありません。
これらの命令を使う手段として、
・アセンブラ
・インラインアセンブラ
・コンパイラの intrinsic 命令
があります。

MSDN MMX, SSE, and SSE2 Intrinsics

コンパイラ命令として intrinsic を使うためには、/Oi オプションが必要で
それぞれ専用のヘッダファイルを include します。

・MMX mmintrin.h
・SSE xmmintrin.h
・SSE2 emmintrin.h

ネットで調べると SSE3/SSSE3 命令を使う場合はさらに

・SSE3 pmmintrin.h
・SSSE3 tmmintrin.h

が必要になるとのこと。でも VisualStudio2005 (VC8) の include には
これらのファイルが見つかりません。
さらにもう少し調べてみたところ、VC8 では intrin.h を include するだけで
SSE~SSE3 の命令が使えるようになるそうです。

例えば SSE3 で追加された水平加算命令 haddps は、intrinsic では
_mm_hadd_ps() となります。実際にコンパイルして実行できました。

SSE は基本的に垂直演算用に設計されているため、水平加算などの命令は
ほとんど用意されていませんでした。

確かに Matrix など常に複数のベクトルがセットで用いられる場合は
垂直演算だけで4個いっぺんの積和で求まるのでそれほど困りませんでした。
でも単発で出てくる細かい積和や内積では、dp3/4 一発で済む shader と
違って結構命令数がかかってしまいます。_mm_hadd_ps() はこんなときに
重宝しそうです。

水平加算とはいえ2要素単位で合計を求める命令なので、4要素全部の加算には
2命令分の実行になります。内積の場合

__m128 va= _mm_load_ps( &_va );
__m128 vb= _mm_load_ps( &_vb );
va= _mm_mul_ps( va, vb );
va= _mm_hadd_ps( va, va );
va= _mm_hadd_ps( va, va );

と命令数がかなり減ります。アセンブラで比較すると

; SSE
movps xmm0, XMMWORD PTR _va
movps xmm1, XMMWORD PTR _vb
mulps xmm1, xmm0
movps xmm0, xmm1
shufps xmm0, xmm1, 01001110b
addps xmm0, xmm1
movps xmm1, xmm0
shufps xmm1, xmm0, 10110001b
addps xmm1, xmm0

; SSE3
movps xmm0, XMMWORD PTR _va
movps xmm1, XMMWORD PTR _vb
mulps xmm0, xmm1
haddps xmm0, xmm0
haddps xmm0, xmm0

演算部の 7命令が 3命令になっています。
非常に便利になった気がしますが、でもこれも SSE4 が出るまでの話。
もうすぐ登場予定の SSE4 ではそのものずばり DotProduct 命令 dpps が
追加されるので、上記の演算3命令すら1つで済んでしまいます。

Intel SSE4 Programming Reference

こちらの SSE4 命令の詳細マニュアルによると、dpps は単に内積の積和を
演算するだけでなく、入力値と出力値のマスクも可能になっています。
Shader の swizzle ほど入力の自由度はないですが、それでも 3x3 内積を
簡単に指定できます。
今までわざとプログラマに意地悪しているんじゃないかと思えるほど
使いづらい仕様だった SSE 命令が、SSE4 では急にフレンドリーになったように
感じます。突然命令が複雑化したのでレイテンシは増えそうです。
実際に登場したらどれくらいの速度になるか、どれだけスループットを稼げるか
みものでしょう。

他にも SSE4 で便利そうな命令として extractps, insertps があります。

extractps は、128bit 中任意のスカラー(32bit 値)の取出しです。
例えば shader でいう src.y , src.w といった単一要素の参照を簡単にします。
これまで無かったのが不思議です。
Opcode は 6byte ほど。さすがに長くなっています。

insertps はさらに 128bit 中任意の 32bit に値を書き込みもできます。
一度に 1要素のアクセスに制限されますが、任意のスカラー値のコピーが
1命令でできるようになりました。

dest.z = src.y とかできるわけですね。


このあたりのドキュメントを読んでいたせいか、今朝

 ものすごい簡単で便利な命令が追加されたけど、
 プリフィックスだらけで命令長が 20byte 以上、
 めちゃめちゃ長くなっている。

・・という夢を見ました。
未来の x86 拡張命令はいったい何 byte になってるんでしょうか。


SSE についてのメモ(1)


今のパソコンの CPU にはほぼ SIMD 命令の SSE がついています。
単精度の浮動小数であれば 4要素同時に演算することが可能で、
ちょうど shader のベクトルレジスタに似ています。

ところが SSE は shader とは違い、2オペランドの破壊型演算命令だったり
各要素をばらばらにアクセスするのが困難だったり、
水平演算が苦手だったりと意外にとっつきにくいものでした。

また演算時のスループットも当初 2以上で、ただただその命令に
置き換えれば速くなるとは限りませんでした。
特徴や使いどころの傾向をつかむまでは苦労させられます。

CPU の進化とともに SSE も強化されており、SSE2、SSE3~ 等と
命令も実行効率も進化しています。

・SSE Pentium3~ 128bit xmm0~7 レジスタ追加、float×4 の演算 等
・SSE2 Pentium4~ double ×2、xmm 整数演算 等
・SSE3 Pentium4~ 実数用水平演算命令など
・SSSE3 Core2~ xmm 整数用水平演算命令など
・SSE4 (Penryn)~ 最大最小検索、DotProduct、bit数カウント、CRC32命令など


最近あらためて、これらの命令などをいろいろ調べていました。
これはただのメモです。

VisualStudio は FPU の代わりに SSE を使って浮動小数点演算を行う
ことができます。2005 の場合コンパイルオプション /arch:SSE or /arch:SSE2
の指定で FPU 命令の置き換えになります。また演算時の精度を /fp で
指定します。これは必要に応じて SSE/SSE2 命令を使ってくれるので便利です。

たとえば /arch:SSE2 /fp:fast 時、float の int へのキャストは
cvttss2si 命令になりました。SSE 指定がない場合は _ftol2 の呼び出し
になっています。

int castInt( float a )
{
return (int)a;
}

// ↓

cvttss2si eax, DWORD PTR _a$[esp-4]

Windows の x64 では FPU は使われずに最初から SSE を使った演算のみの
サポートとなるようです。(32bit 互換モードを除く) なので今後ますます
SSE/SSE2 が多用されていくことになるでしょう。

float から _int64 へのキャストも x64 の場合は cvttss2si 命令ですが、
32bit では実行できないので _ftol2 の呼び出しになっていました。


コンパイル時のオプション /fp:fast のあるなしは演算精度に影響します。
MSDN /fp (浮動小数点の動作の指定)

例えば全部 float の演算の場合、/fp:fast 無し (/fp:precise) では

float va, vb, vz;
float xs= va * vb + vz;

movss xmm0, DWORD PTR vb
movss xmm1, DWORD PTR va
cvtps2pd xmm0, xmm0 ; double ← float
cvtps2pd xmm1, xmm1 ; double ← float
mulsd xmm0, xmm1
movss xmm1, DWORD PTR vz
cvtps2pd xmm1, xmm1 ; double ← float
addsd xmm0, xmm1
cvtpd2ps xmm0, xmm0 ; float ← double

演算途中の値が倍精度で保たれるよう事前に変換が発生しています。
これを /fp:fast でコンパイルすると単精度のまま演算が行われている
ことがわかります。

movss xmm0, DWORD PTR vb
mulss xmm0, DWORD PTR va
addss xmm0, DWORD PTR vz

さらに

double xd= (double)va * (double)vb + (double)vz;

でも結果は上記と全く同じコードで、演算後に cvtps2pd で変換していました。
予想よりもずっと大胆な最適化なので /fp:fast を使うときは注意が必要でしょう。

ここで少々不思議なのは movss したスカラー値を倍精度変換するために
cvtps2pd が使われていることです。パックしてから変換を最適化している
わけでもないので、本来は cvtss2sd で十分だと思われます。

intel 日本語技術資料のダウンロード

ここからダウンロードできる「ia32命令 レイテンシトスループット」や
IA-32 インテルR アーキテクチャー最適化リファレンス・マニュアル」を読むと

ia32.pdf Pentium4
・cvtps2pd 10/4 (レイテンシ/スループット)
・cvtss2sd 14/3

ia32_final.pdf Pentium4
・cvtps2pd 2/2
・cvtss2sd 8/2

ia32_final.pdf Pentium-M
・cvtps2pd 2+1/3
・cvtss2sd 2/2

Pentium4 では cvtps2pd よりも cvtss2sd 命令のほうがレイテンシがかなり
大きいことがわかります。ドキュメントによって Pentium4 の値が異なるのは
おそらく cpu core の世代の違いだと思われます。

またドキュメントにはもう少し興味深いこともかかれています。
movsd xmm, xmm 等レジスタの 64bit 分化しか更新せずに上位 64bit が保持
される命令の場合は、上位 bit の結果に依存関係が発生してしまうそうです。
そのため実行の並列化が阻害されます。
レジスタを 128bit 分すべて書き換える命令の方が依存関係が発生しないため
レジスタのリネーミングも可能で好ましいとのことです。なるほど。