* 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]
-- インテルの 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 倍

のスピードアップが図られたことになります.



トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS