ぬうぱんの備忘録

技術系のメモとかいろいろ

作った曲一覧はこちら

gccでintrinsicsでSSEでベクタライズする時の簡易的なまとめ

この記事は

 intrinsicsを使ってSSEでベクタライズするのに必要な足がかりを自分用にまとめたものです。

そもそもSSEって何

 SIMD(http://ja.wikipedia.org/wiki/Streaming_SIMD_Extensions)を実現する拡張命令セットの名前。いろいろ種類があるしバージョンもある。基本的な考え方をすごく乱暴に言うと、floatの掛け算を4回繰り替えすよりも4つのfloatの掛け算を一息にやったら早くね? という感じ。

intrinsicsって何

 前述の通り、SSEとかAVXはCPUの命令なので直接使おうと思うと自分でアセンブラを書かなくてはいけない。が、既存のC++コードを何とかしたいというのが調べてる動機なのでgccSIMD intrinsicsを使うことにする。このSIMD intrinsicsはコンパイラーの組み込み関数で、こいつらを使うことでC/C++ソースコードからSSEを利用することができる。
 intrinsicsではgccVC++で互換性がある(一部互換性が無いかもしれない?)ようなので移植で苦労しなさそうなのもポイント。
 なお、コンパイラーの機能を利用してCPUの拡張命令を使用するので、使いたい拡張命令セットをCPUとコンパイラーの両方でサポートしている必要がある。

いろいろ試してみる環境

 今回は以下の環境でいろいろ試してます

SSEいろいろ

 SSEはSIMD拡張命令セットいろいろをまとめた総称なのでいろいろ種類がある。時系列順に並べると

みたいな感じ。下のほうが新しい。それぞれにバージョンが存在する。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
  • -msse4.1, -msse4.2, -mavxはSSEは4.2まで全部とAVXが使えることを意味している。
  • -mno-avx2, -mno-fma, ...はAVX2とFMA類が使えないことを意味している。

makefileの設定

 コンパイル時には直接-msse4.2とか設定してもいいのですが、-march=nativeを設定しておきましょう。コレを設定しておかないとソース中で拡張命令を使えません。

ヘッダーについて

 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プログラミングをする場合は以下のような流れになります

  1. メモリからレジスタに必要な値をロード
  2. レジスタ上で四則演算とかビット演算とか
  3. 結果が格納されているレジスタの中身をメモリにストア

 普段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);

*1:aligned(n