ARM Cortex-A8 の NEON と浮動小数演算最適化

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 は上のプログラムを正しく変換できるように必要な命令の分しか
作っていません。つまりまだ未完成です。対応してない命令があるのでどんなプログラムでも
変換できるわけではありません。
それでもきちんと効果があるので、このまま対応命令を増やせば浮動小数演算を多用した
アプリケーションも大幅な高速化が期待できそうです。

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 と速度比較