gcc-4.x の SIMD 命令サポート その2

新しいマシンを買い,Linux マシンが Celeron D にグレードアップしてしまいました. というわけで,今回は SSE/SSE2 について.

SSE レジスタ

SSE では 128 ビット長の専用レジスタを 8 本新設しています. つまり,前回の 3DNow! とは違い,x87 演算と分離する必要はありません.

で,レジスタの中身ですが,SSE レジスタ 1 本に

  • SSE では 32bit 単精度 (float) 型を 4 本
  • SSE2 では 64bit 倍精度 (double) 型を 2 本

を格納し,SIMD 演算を行うことができます.

参考文献

参考となる書籍など.

おおまかな枠組み

前回と一緒で

  1. SIMD のレジスタにあった型の変数を定義して
  2. ビルトイン関数で演算を記述
  3. -mXXX オプションを付けてコンパイル

です.

プログラム例

前回と同様にベンチマークを兼ねてプログラムを作ってみました.

お題も前回と同じく積和演算です.

型宣言

まずは 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 命令

まずは 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 で割り切れるアドレスでなければなりません. うっかりすると忘れてしまうので注意しましょう.

ベンチマーク環境

結果に先立って,ベンチマーク環境について書いときます.

$ 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:

ベンチマーク結果

まずは最適化オプションなしでこんな感じ.

$ 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 倍

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


添付ファイル: filebenchmark.c 2447件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2008-09-17 (水) 20:29:02 (5722d)