この記事は
intrinsicsを使ってSSEでベクタライズするのに必要な足がかりを自分用にまとめたものです。
そもそもSSEって何
SIMD(http://ja.wikipedia.org/wiki/Streaming_SIMD_Extensions)を実現する拡張命令セットの名前。いろいろ種類があるしバージョンもある。基本的な考え方をすごく乱暴に言うと、floatの掛け算を4回繰り替えすよりも4つのfloatの掛け算を一息にやったら早くね? という感じ。
intrinsicsって何
前述の通り、SSEとかAVXはCPUの命令なので直接使おうと思うと自分でアセンブラを書かなくてはいけない。が、既存のC++コードを何とかしたいというのが調べてる動機なのでgccのSIMD intrinsicsを使うことにする。このSIMD intrinsicsはコンパイラーの組み込み関数で、こいつらを使うことでC/C++のソースコードからSSEを利用することができる。
intrinsicsではgccとVC++で互換性がある(一部互換性が無いかもしれない?)ようなので移植で苦労しなさそうなのもポイント。
なお、コンパイラーの機能を利用してCPUの拡張命令を使用するので、使いたい拡張命令セットをCPUとコンパイラーの両方でサポートしている必要がある。
SSEいろいろ
SSEはSIMD拡張命令セットいろいろをまとめた総称なのでいろいろ種類がある。時系列順に並べると
- SSE
- AVX
- FMA
みたいな感じ。下のほうが新しい。それぞれにバージョンが存在する。SSEは128bitレジスタでAVXからは256bitレジスタ。AVXとSSEには互換性があるようだが切替時にペナルティが存在するらしい。
環境の調べ方
まずは自分の環境でどの命令セットを使えるのか調べてみる。
gccに-march=nativeを渡すと自動的使える命令セットを調べて適切なフラグを設定してくれる。ということは設定されるフラグを展開すればいいのでは? ということで以下の要領で展開してみる
$ gcc -E -v -march=native - 2>&1 | grep cc1 /usr/lib/gcc/x86_64-linux-gnu/4.7/cc1 -E -quiet -v -imultiarch x86_64-linux-gnu - -march=corei7-avx -mcx16 -msahf -mno-movbe -maes -mpclmul -mpopcnt -mno-abm -mno-lwp -mno-fma -mno-fma4 -mno-xop -mno-bmi -mno-bmi2 -mno-tbm -mavx -mno-avx2 -msse4.2 -msse4.1 -mno-lzcnt -mrdrnd -mf16c -mfsgsbase --param l1-cache-size=32 --param l1-cache-line-size=64 --param l2-cache-size=8192 -mtune=generic -fstack-protector
ヘッダーについて
intrinsicsを使用するためには当然ヘッダーファイルをインクルードする必要があります。SSEのバージョンごとに分かれているようなので一覧を
#include <xmmintrin.h> //SSE #include <emmintrin.h> //SSE 2 #include <pmmintrin.h> //SSE 3 #include <tmmintrin.h> //SSSE 3 #include <smmintrin.h> //SSE 4.1 #include <nmmintrin.h> //SSE 4.2
へっだを見るとわかるのですが、間違ってフラグが有効になっていないバージョンをインクルードした場合はコンパイルエラーになるようです
ソースからどのフラグがONになっているか知る方法
-msse4.1みたいなフラグが渡されるとコンパイラ側で``__SSE4_1__''みたいなマクロが定義されるようです。これを#ifdefなんかで調べればいいんじゃないでしょうか。
大雑把な処理の流れと注意
intrinsicsを用いてSIMDプログラミングをする場合は以下のような流れになります
普段C/C++を書いていてまず意識することのないレジスタへのロードとメモリへのストアを自分で指定する必要があります。
ロード/ストアするアドレスは16バイトでアラインされていることが望ましいです。16バイトアラインされてないアドレスからでも読み込める命令もありますが、アラインされているほうが高速です。ということは、変数はアラインメントに気をつけて定義する必要があるようです。
16バイトアラインメントを保証する
前述の通り、処理の対象となるデータは16バイトアラインメントに乗っていることが望ましいです。staticな変数やスタック上の変数の場合は以下のように定義すれば良いようです。
//配列の先頭アドレスが16バイトアラインされる float hoge[4]__attribute__((aligned(16))); //同じことをtypedefで typedef float float_4a[4] __attribute__((aligned(16))); float_4a hoge; //構造体の先頭アドレスとsizeof(piyo)が16バイトアラインされる //メンバのアラインメントについてはノータッチ struct piyo{ float foo; float bar; }__attribute__((aligned(16))); //hogeは16バイトアラインされる piyo hoge; //pは16バイトアラインされる piyo* p = new piyo;
arigned(XXX)の付いたstructをnewした場合はアライン済みのアドレスが確保されるようです。
もし、先頭アドレスがアラインされた任意の長さの配列を確保したい、という場合は
#include <stdlib.h> float* hoge = NULL; posix_memalign(&hoge, 16, sizeof(float)*4); free(hoge);
みたいな感じでアラインつきmallocを使えばOK。
VC++の場合は__attribute__ではなくて#pragmaを使用し、_aligned_mallocを使うようです。
レジスタのストア/ロード
まずはfloat値をレジスタにストアしてそこからメモリにロードしてみましょう。
float Hoge[4] __attribute__((aligned(16))); //ロード元 float Piyo[4] __attribute__((aligned(16))); //ストア先 __m128 Acc; //レジスタ //入力値を設定 Hoge[0] = 1; Hoge[1] = 2; Hoge[2] = 3; Hoge[3] = 4; //ロード>ストア Acc = _mm_load_ps(Hoge); _mm_store_ps(Piyo, Acc); //結果を確認 for(int i=0; i<4; ++i){ cout << Hoge[i] << endl; }
超簡単っすね。標準出力に"1 2 3 4"が出てくるでしょう。このケースではfloatの計算でしたが、型が変わってくると使う型や関数が変わってきます。
C++型 | レジスタ型 | ロード命令 | ストア命令 |
---|---|---|---|
float | __m128 | _mm_load_ps() | _mm_store_ps() |
double | __m128d | _mm_load_pd() | _mm_store_pd() |
整数 | __m128i | _mm_load_si128() | _mm_store_si128() |
命令についてはコレ以外にもいろいろあって、名前は動作の違いでちょっとづつ変わってきます。
命令は末尾のps, pd, si128が変わります。precision single, precision doubleはわかるけどsiって何。
整数の場合、__m128i*からロードすることになっていますが、コレは
int Hoge[4] __attribute__((aligned(16))); __m128i Acc; Acc = _mm_load_si128(reinterpret_cast<__m128i*>(&Hoge));
のようにキャストしてしまえばOKのようです。整数型を一括して扱うための措置ですかね?
intrinsicsのリファレンス
使用可能なintrinsicsの一覧はこ↑こ↓(https://software.intel.com/sites/landingpage/IntrinsicsGuide/)で確認できます。拡張命令セットとか命令のカテゴリでフィルタリングしてブラウジング可能。まよったらとりあえずココを参照しましょう。
TIPS : 浮動小数点から整数への丸めの方法について
_mm_cvtps_epi32()のような浮動小数点から整数への変換を行うintrinsicでは当然値の丸めを行う。この時の丸めの方法はMXCSRレジスタの値によって変わるらしい。このMXCSRレジスタを書き換えてやれば丸めの方法を変えられるという事になる。
レジスタの取得/設定は"_mm_getcsr()"/"_mm_setcsr()"を使用すれば良いのだがこのMXCSRレジスタの中のRCフラグのみを書き変える必要がある。そのため、ANDでRCフラグを0にしてからORで必要なフラグを設定することになる。なお、デフォルトでは四捨五入になる模様。
※追記 MXCSRレジスタの設定値思いっきり間違えていました・・・。ネギ (id:ad2217)さんご指摘ありがとうございます。
//MXCSRレジスタを取得 unsigned long mxcsr = _mm_getcsr(); //四捨五入 mxcsr &= 0xFFFF9FFF; mxcsr |= 0x00000000; //マイナス側の最も近い整数 mxcsr &= 0xFFFF9FFF; mxcsr |= 0x00002000; //プラス側の最も近い整数 mxcsr &= 0xFFFF9FFF; mxcsr |= 0x00004000; //切り捨て mxcsr &= 0xFFFF9FFF; mxcsr |= 0x00006000; //RCフラグを書き換えたMXCSRレジスタを設定 _mm_setcsr(mxcsr);
参考
gccでsse: 鍛えども鍛えども我がお腹細くならず
Streaming SIMD Extensions - Wikipedia
Re: no subject
組み込み関数(intrinsic)によるSIMD入門
[https://cell.fixstars.com/ps3linux/index.php/%E6%95%B4%E5%88%97%E3%81%97%E3%81%9F%E3%83%A1%E3%83%A2%E3%83%AA%E3%82%92%E9%9D%99%E7%9A%84%E3%81%AB%E7%A2%BA%E4%BF%9D%E3%81%99%E3%82%8B_attribute_*1:title]
Intel Intrinsics Guide
CVTPS2DQ--Convert Packed Single-Precision Floating-Point Values to Packed Doubleword Integers
*1:aligned(n