ARM も x86 と同じように、これまで様々な命令セットの拡張が行われてきました。
例えば乗算一つとっても多数の命令が存在しており、どれを使えばよいかわからなくなる
ことがあります。
x86 に FPU と SSE 命令が共存しているように、ARM にも VFP や NEON 命令があります。
どちらの命令を使っても、単精度の浮動小数演算が可能です。
Cortex-A8 (ARM v7) の場合: 浮動小数演算
VFP 低速 倍精度演算対応 IEEE754 準拠 NEON 高速 単精度のみ SIMD
NEON 命令は VFP 命令も混在しており区別が付きにくいことから、それぞれ演算速度を
調べてみました。
下記の表は気になる命令のみピックアップして調べたものです。
4 G 個 (40億個) の命令を実行した場合の時間の実測値です。
01: vld1.32 {d[0]},[r] 20.16 sec *1 02: vld1.32 {d[0]},[r] 15.14 sec *2 03: vldr.32 s,[r] 15.06 sec *1 04: vldr.32 s,[r] 10.04 sec *2 05: vldr.64 d,[r] 10.18 sec 06: vdup.32 d,r 5.90 sec 07: vdup.32 q,r 5.90 sec 08: vdup.32 d,d[0] 5.02 sec 09: vdup.32 q,d[0] 5.02 sec 10: vmov.32 d[0],r 15.10 sec *1 11: vmov.32 d[0],r 10.08 sec *2 12: vmov.32 r,d[0] 7.03 sec 13: vmov d,r,r 11.79 sec 14: vmov r,r,d 12.05 sec 15: vcvt.s32.f32 d,d 5.01 sec 16: vcvt.s32.f32 q,q 10.03 sec 17: vcvt.f32.s32 s,s 45.30 sec VFP 18: vcvt.f32.s32 d,d 3.76 sec 19: vcvt.f32.s32 q,q 10.04 sec 20: vadd.f32 d,d,d 5.02 sec 21: vadd.f32 q,q,q 10.04 sec 22: vmla.f32 d,d,d 5.02 sec 23: vmla.f32 q,q,q 10.04 sec 24: vrecpe.f32 d,d 5.02 sec 25: vrecpe.f32 q,q 10.10 sec 26: vrecps.f32 d,d,d 5.08 sec 27: vrecps.f32 q,q,q 10.09 sec r = ARM レジスタ r0~ s = VFP レジスタ s0~s31 d = NEON/VFP レジスタ d0~d31 q = NEON レジスタ q0~q15 *1 = インターリーブ無し *2 = インターリーブあり
ARM レジスタから NEON/VFP レジスタへのデータ転送はいくつかの手段が考えられます。
13: vmov d,r,r 11.79 sec 14: vmov r,r,d 12.05 sec
この命令は NEON の 64bit d0~d31 レジスタと、ARM レジスタ r0~ 2個を相互に
転送するものです。ARM 側は必ず 2個セットの 64bit でなければならず、動作時間も
比較的かかっています。
10: vmov.32 d[0],r 15.10 sec *1 11: vmov.32 d[0],r 10.08 sec *2 12: vmov.32 r,d[0] 7.03 sec
上の vmov.32 は NEON の d0~d31 レジスタのうち半分、32bit 分のみ転送する命令です。
NEON レジスタは下記のように割り当てられています。
NEON ビュー +-------------------------------+ | q0 | 128bit ( ~ q15 , 16個 ) +---------------+---------------+ | d0 | d1 | 64bit ( ~ d31 , 32個 ) +-------+-------+-------+-------+ | d0[0] | d0[1] | d1[0] | d1[1] | 32bit ( ~ d31[1] , 64個 ) +-------+-------+-------+-------+
全く同じ領域を VFP レジスタとしてもアクセスすることが出来ます。
VFP ビュー +---------------+---------------+ | d0 | d1 | 64bit ( ~ d31 , 32個 ) +-------+-------+-------+-------+ | s0 | s1 | s2 | s3 | 32bit ( ~ s31 , 32個 ) +-------+-------+-------+-------+
VFP ビューでは 32bit のデータを sレジスタとして個別にアクセスできます。
64bit dレジスタの扱いはほぼ同じです。
命令フィールドの制限から、レジスタ番号は 0~31 の範囲でなければなりません。
つまり 32bit レジスタが 64 個あるにもかかわらず、s レジスタとしてアクセス出来る
のは半分の s0~s31 だけです。
NEON ビューの 32bit スカラ要素では、このようなアクセス制限が無いことがわかります。
NEON ビューと VFP ビューの重要かつ大きな違いがもう一つあります。
NEON では dレジスタの 64bit 単位でデータを扱うということ。
VFP は sレジスタ単位、つまり 32bit 単位でデータを扱います。
よって上の命令
10: vmov.32 d[0],r 15.10 sec *1
は NEON d レジスタの 32bit 分、半分の領域にしか書き込みを行いません。
このとき d レジスタは、残り半分のデータを保存しなければならなくなるため
デステネーションレジスタにも依存が発生します。
いわゆるパーシャルレジスタストールです。
SSE で SS 命令よりも、完全に置き換える PS 命令の方が速いのと同じです。
実際に測定してみると、下記のようにデスティネーション側レジスタに同じ d レジスタ
を指定するとパイプラインストールが発生します。これが 10: の vmov.32 の値です。
; 同じ d0 に部分書き込みを行うためストールする。10: vmov.32 ~ 15.06 sec vmov.32 d0[0],r2 vmov.32 d0[1],r3 vmov.32 d0[0],r2 vmov.32 d0[0],r3 ~
; 異なるレジスタへ交互に書き込む場合。11: vmov.32 ~ 10.04 sec vmov.32 d0[0],r2 vmov.32 d1[0],r2 vmov.32 d2[0],r2 vmov.32 d3[0],r2 ~
レジスタを置き換えた 11: の方では 15 sec → 10 sec と速くなるため、1cycle 分の
遅延が発生していることがわかります。
全く同じことが 03: の vldr.32 s,[r] でも発生していることがわかりました。
03: vldr.32 s,[r] は VFP 命令のはずですが、実行時間を見ると NEON の演算ユニットで
実行しているようです。
NEON には即値アドレスのメモリから 32bit スカラを読み込む命令がないので
この命令を多用しても大丈夫そうです。
s レジスタへの書き込みは上の d[x] と同じように 32bit の部分書き込みに相当します。
実際に下記の通りストールが発生しました。
; ストールする ( s0 も s1 も同じ d0 レジスタに相当するため) vldr.32 s0,[r1] vldr.32 s1,[r1] vldr.32 s0,[r1] vldr.32 s1,[r1]
; ストールしない vldr.32 s0,[r1] vldr.32 s2,[r1] vldr.32 s4,[r1] vldr.32 s6,[r1]
また sレジスタなので、後半 d16~d31 エリアへ直接 32bit の値をロードすることができません。
VFP 命令が遅いのは、このようなアーキテクチャの違いも一つの要因かもしれません。
レジスタへのアクセス単位が異なるので、パイプラインが矛盾しないように実行が終わるのを
待っている可能性があります。
最初の測定結果から、ARM レジスタから NEON レジスタへのスカラ転送は vdup を
使うのが最も効率がよいことがわかります。
実行速度も速く d レジスタを完全に置き換えるために不要な依存が発生しません。
メモリからの読み込みは vldr.32 s を用います。後半 d16~d31 エリアへの代入が
必要なら vld1.32 を使うことが出来ますが、この場合利用できるアドレッシングモードに
制限があります。
これらの測定データを元に、VFP 命令を NEON 命令に置換する簡単なスクリプトを
作ってみました。命令置換は下記の方針で処理しています。
・s0~s31 レジスタは d0~d31 レジスタのスカラにマップする。 ・ARM レジスタとの相互転送は下記の命令を使う。 R ← D vmov.32 r,d[0] D ← R vdup.32 d,r ・一般の演算や浮動小数と整数の変換などは NEON の 64bit (float x2) 演算を用いる。 ・メモリからのロードは vldr.32 s,addr ・d レジスタの奇数要素 d0[1]~d31[1] は一時的なテンポラリに使える。 ・どうしても s レジスタを使わなければならない命令ではレジスタ番号を 2倍する。 例えば double への変換や vldr 命令時。 s1 は d1 = (s2,s3) にマッピングされるため。 ・s レジスタの番号が 32 を超える場合は、テンポラリを経由した複数命令に展開する。やむなし ・倍精度演算命令は置き換えない。
gcc は VFP でコンパイルし、出力したアセンブラコード (*.s) をいったんスクリプトに通して
可能な部分を NEON 命令に置き換えます。例えば Makefile は下記のような感じで、
opt_neon.pl を通しています。
%.o: %.cpp $(CC) $(CFLAGS) $< -S -o $*._temp.s $(PERL) opt_neon.pl $*._temp.s > $*.s $(CC) $(CFLAGS) $*.s -c -o $@
void Loop_main( float dd ) { TimerClass timer; timer.Begin(); float sum= 0.0f; for( int i= 0 ; i< 100000000 ; i++ ){ sum= sum * dd + dd; } timer.End( "end" ); printf( "%f\n", sum ); }
上のプログラムを実行した結果は次の通り (NetWalker Cortex-A8 800MHz)
そのまま VFP を使用した場合 2.39 秒 オプティマイザを通した場合 1.01 秒
スクリプト opt_neon.pl は上のプログラムを正しく変換できるように必要な命令の分しか
作っていません。つまりまだ未完成です。対応してない命令があるのでどんなプログラムでも
変換できるわけではありません。
それでもきちんと効果があるので、このまま対応命令を増やせば浮動小数演算を多用した
アプリケーションも大幅な高速化が期待できそうです。
将来的にはおそらく gcc の方に、スカラ演算でも NEON コードだけを生成するオプションが
追加されるのではないでしょうか。
試していませんが iPhone 3GS や iPod Touch 3G でももしかしたら NEON 最適化が
有効かもしれません。
NetWalker i.MX515 Cortex-A8 (ARM v7-A) 800MHz AMD Z430 iPhone 3GS S5PC100 Cortex-A8 (ARM v7-A) 600MHz PowerVR SGX 535
関連エントリ
・NetWalker PC-Z1 Cortex-A8 の NEON 命令とメモリ速度
・SSE の浮動小数演算速度
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較