* gcc-4.x の SIMD 命令サポート その2 [#ae8cd212] 新しいマシンを買い,Linux マシンが Celeron D にグレードアップしてしまいました. というわけで,今回は SSE/SSE2 について. ** SSE レジスタ [#a1f8ecee] SSE では 128 ビット長の''専用''レジスタを 8 本新設しています. つまり,[[前回>日記/2008-09-03/gcc-4.x での SIMD 命令サポート]]の 3DNow! とは違い,x87 演算と分離する必要はありません. で,レジスタの中身ですが,SSE レジスタ 1 本に - SSE では 32bit 単精度 (float) 型を 4 本 - SSE2 では 64bit 倍精度 (double) 型を 2 本 を格納し,SIMD 演算を行うことができます. ** 参考文献 [#n4e9b777] #af_amazon(4789833429,right) 参考となる書籍など. - gcc-4.3 の info -- SIMD 命令の gcc 上の取扱いについて書いてあります. - [http://developer.intel.com/products/processor/manuals/index.htm Intel® 64 and IA-32 Architectures Software Developer's Manuals] - [http://developer.intel.com/products/processor/manuals/index.htm Intel® 64 and IA-32 Architectures Software Developer's Manuals] -- インテルの web サイトにある,IA-32 アーキテクチャのマニュアルです.SSE/SSE2 については Volume I に書いてあります. - [http://www.amazon.co.jp/dp/4789833429 x86アセンブラ入門―PC/ATなどで使われている80x86のアセンブラを習得] -- CQ 出版社から出ている本.もちろん日本語です. #clear ** おおまかな枠組み [#ea83c11a] 前回と一緒で + SIMD のレジスタにあった型の変数を定義して + ビルトイン関数で演算を記述 + -mXXX オプションを付けてコンパイル です. ** プログラム例 [#gee400c0] 前回と同様にベンチマークを兼ねてプログラムを作ってみました. #ref(benchmark.c) お題も前回と同じく積和演算です. ** 型宣言 [#ud57c751] まずは SSE での「単精度 x 4 本」の構成の変数の型宣言から. typedef float v4sf __attribute__ ((vector_size(16))) __attribute__((aligned(16))); 前回と違っておしりに __attribute__((aligned(16))) なんて属性がついてますね. これは > 変数を 16 で割り切れるアドレスに割り当てよ という属性です. これは,実は SSE レジスタのロード・ストア時においてこの様な制限があるために,このような属性を追加しています. この制限を無視するとどうなるかというと,Segmentation fault が発生します. ARM や MIPS などの変数のアラインメントと似たような感じです. ここで 1 つ注意.この aligned(16) 属性は v4sf a; のような変数の宣言では有効に機能しますが v4sf *a = malloc ( sizeof(v4sf) ); のような動的メモリ割り当てではうまく働きません.また float a[2]; *(v4sf *)&a[0] = b; のようなキャストもだめです. つまり,既に割り当てられている領域を「16 で割り切れるアドレスに配置換え」する力はありません. ついでに「倍精度 x 2」の型宣言. typedef double v2df __attribute__ ((vector_size(16))) __attribute__((aligned(16))); 制限・注意は v4sf 型と一緒です. ** SSE/SSE2 命令 [#l1921250] まずは v4sf 型について. これらは SSE 命令セットに属します. :v4sf __builtin_ia32_addps (v4sf a, v4sf b)| v4sf 型に対する SIMD の足し算です.a + b の結果を返します. :v4sf __builtin_ia32_mulps (v4sf a, v4sf b)| v4sf 型に対する SIMD 掛け算です.a * b の結果を返します. v2df 型について.これらは SSE2 命令セットです. :v2df __builtin_ia32_addpd (v2df a, v2df b)| v2df 型に対する SIMD 足し算.a + b の結果を返します. :v2df __builtin_ia32_mulpd (v2df a, v2df b)| v2df 型に対する SIMD 掛け算.a * b の結果を返します. これらの明示的なビルトイン関数以外にも例えば v2sf a = { 1.0, 2.0, 3.0, 4.0 }; のような代入文でも SSE 命令が使用されます. つまり,単なる代入においても右辺と左辺の変数領域は 16 で割り切れるアドレスでなければなりません. うっかりすると忘れてしまうので注意しましょう. ** ベンチマーク環境 [#nd849a52] 結果に先立って,ベンチマーク環境について書いときます. $ cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 15 model : 4 model name : Intel(R) Celeron(R) CPU 2.66GHz stepping : 9 cpu MHz : 2667.096 cache size : 256 KB fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 5 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts \ acpi mmx fxsr sse sse2 ss ht tm pbe lm constant_tsc pebs bts pni monitor ds_cpl tm2 cid cx16 xtpr \ lahf_lm bogomips : 5340.17 clflush size : 64 power management: ** ベンチマーク結果 [#ucd2bcab] まずは最適化オプションなしでこんな感じ. $ gcc -msse2 benchmark.c $ ./a.out SSE double: 9.620000 3998000.000000 norm double: 20.890000 3998000.000000 SSE float: 4.910000 3998000.000000 norm float: 20.560000 3998000.000000 中央のカラムが演算にかかった CPU 時間で,単位は秒です. 最適化オプションを付けてみましょう. $ gcc -msse2 -O2 benchmark.c $ ./a.out SSE double: 2.680000 3998000.000000 norm double: 4.910000 3998000.000000 SSE float: 1.070000 3998000.000000 norm float: 4.760000 3998000.000000 かなり違いますね. ループ展開もしてみましょう. $ gcc -msse2 -O2 -funroll-loops benchmark.c imai@JR0BAK:~/progs/sse$ ./a.out SSE double: 2.380000 3998000.000000 norm double: 4.850000 3998000.000000 SSE float: 1.060000 3998000.000000 norm float: 4.670000 3998000.000000 ちょっぴり速くなりましたが,3DNow! のときのように劇的な効果はないようです. ループ展開したことで SSE/SSE2 の演算では多くの SSE レジスタを使用するコードが生成されています. にもかかわらずあまり速度が上がってないのは,実は SSE/SSE2 でも演算ユニットは 1 セットしかないのかもしれません. 最後の例で SSE/SSE2 と x87 の速度を比較すると,SSE/SSE2 の SIMD 演算を用いたことにより - 単精度で 4.4 倍 - 倍精度で 2.0 倍 のスピードアップが図られたことになります.