Quo vasistis, NEON instructiones?

# 概要
ARMプロセッサの多くにはNEONというSIMD命令が搭載されています。
ARM Cortex-A7などの32ビットARMプロセッサの場合、次のようにオプション指定することでNEON命令を使用したコードを生成できます。

```
$ gcc -o test -O3 -mfpu=neon-vfpv4 -funsafe-math-optimizations test.c
```
ところが、NanoPi-NEO4(RK3399、Cortex-A72x2+Cortex-A53x4)上のgccで同様にオプション指定してNEON命令をコンパイラに使用させようとすると、

```
$ cc -o test -Ofast -mfpu=neon-fp-armv8 -funsafe-math-optimizations teest.c
cc: error: unrecognized command line option ‘-mfpu=neon-fp-armv8’
```

「-mfpu=neon-fp-armv8なんてオプションねーよ」

と怒られます。

ああ、NEON命令よ、何処に行った? (表題のラテン語)

# 普通にコンパイルすると

NEON命令によるベクトル化の例として、次のプログラムをコンパイルしてみることにします。

```test.c
#include <stdio.h>

main()
{
	int i,j;
	float x[8]={ 0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0 };
	float y[8]={ 10.0,11.0,12.0,13.0,14.0,15.0,16.0,17.0 };
	float z[8];
	for(i=0;i<8;i++)
	{
		z[i]=x[i]*y[i];
	}
	for(i=0;i<8;i++)
	{
		printf("%8.3f\n",z[i]);
	}
}
```

単純に単精度実数の配列の要素ごとの積をとり、別の単精度実数の配列にストアするだけのプログラムです。

まずはこれを普通にコンパイルして、どのようなアセンブリコードが吐き出されるか観察して味わってみましょう。コンパイラオプションとしては「-O2」を指定します。

```test.s
	.arch armv8-a
	.file	"test.c"
	.text
	.align	2
	.global	main
	.type	main, %function
main:
	stp	x29, x30, [sp, -160]!
	add	x29, sp, 0
	stp	x19, x20, [sp, 16]
	stp	x21, x22, [sp, 32]
	adrp	x0, :got:__stack_chk_guard
	ldr	x0, [x0, #:got_lo12:__stack_chk_guard]
	ldr	x1, [x0]
	str	x1, [x29, 152]
	mov	x1,0
	adrp	x0, .LANCHOR0
	add	x0, x0, :lo12:.LANCHOR0
	ldp	x2, x3, [x0]
	stp	x2, x3, [x29, 56]
	ldp	x2, x3, [x0, 16]
	stp	x2, x3, [x29, 72]
	ldp	x2, x3, [x0, 32]
	stp	x2, x3, [x29, 88]
	ldp	x0, x1, [x0, 48]
	stp	x0, x1, [x29, 104]
	mov	x0, 0
	add	x3, x29, 120
	add	x2, x29, 56
	add	x1, x29, 88
.L2:
	ldr	s0, [x0, x2]
	ldr	s1, [x0, x1]
	fmul	s0, s0, s1
	str	s0, [x0, x3]
	add	x0, x0, 4
	cmp	x0, 32
	bne	.L2
	mov	x19, 0
	add	x22, x29, 120
	adrp	x20, .LC2
	add	x20, x20, :lo12:.LC2
	mov	w21, 1
.L3:
	ldr	s0, [x22, x19, lsl 2]
	fcvt	d0, s0
	mov	x1, x20
	mov	w0, w21
	bl	__printf_chk
	add	x19, x19, 1
	cmp	x19, 8
	bne	.L3
	mov	w0, 0
	adrp	x1, :got:__stack_chk_guard
	ldr	x1, [x1, #:got_lo12:__stack_chk_guard]
	ldr	x2, [x29, 152]
	ldr	x1, [x1]
	eor	x1, x2, x1
	cbnz	x1, .L8
	ldp	x19, x20, [sp, 16]
	ldp	x21, x22, [sp, 32]
	ldp	x29, x30, [sp], 160
	ret
.L8:
	bl	__stack_chk_fail
	.size	main, .-main
	.section	.rodata
	.align	3
	.set	.LANCHOR0,. + 0
.LC0:
	.word	0
	.word	1065353216
	.word	1073741824
	.word	1077936128
	.word	1082130432
	.word	1084227584
	.word	1086324736
	.word	1088421888
.LC1:
	.word	1092616192
	.word	1093664768
	.word	1094713344
	.word	1095761920
	.word	1096810496
	.word	1097859072
	.word	1098907648
	.word	1099431936
	.section	.rodata.str1.8,"aMS",@progbits,1
	.align	3
.LC2:
	.string	"%8.3f\n"
	.ident	"GCC: (Ubuntu/Linaro 7.4.0-1ubuntu1~18.04.1) 7.4.0"
	.section	.note.GNU-stack,"",@progbits

```
こんなコードが出力されました。ちょっと長いですが、配列同士の積を求めている部分は次のようになっています。

```
.L2:
	ldr	s0, [x0, x2]
	ldr	s1, [x0, x1]
	fmul	s0, s0, s1
	str	s0, [x0, x3]
	add	x0, x0, 4
	cmp	x0, 32
	bne	.L2
```

まあ、式そのまんまといいますか、なんといいますか、素朴な味わいのコードです。レジスタs0とs1に値をメモリからコピーしてきてfmul命令で乗算し、str命令でストア、x0に入ってる配列のインデックスをインクリメントしてループしています。

もう少しひねったアセンブリコードが出てくると思っていただけに「式を見ながら普通に手で書いた感じ」のコードだったのがちょっと意外。

# aarch64のSIMD命令セット指定でコンパイルしてみる

次に、このC言語ソースを、aarch64のSIMD命令セット指定でコンパイルしてみます。

冒頭に書いたように、「-mfpu=neon-fp-armv8」を指定してもコンパイルしてくれません。/proc/cpuinfoを見ても「neon」がなく、その代わりに「asimd」があります。なるほど、そのせいだな。

というわけで次のコマンドでコンパイルしてみます。

```
$ cc -S -O3 -march=armv8-a+simd test.c
```

今回はどんなアセンブリコードが出力されたでしょうか。味わってみましょう。

```test.s
	.arch armv8-a
	.file	"test.c"
	.text
	.section	.text.startup,"ax",@progbits
	.align	2
	.p2align 3,,7
	.global	main
	.type	main, %function
main:
	adrp	x0, .LANCHOR0
	add	x0, x0, :lo12:.LANCHOR0
	stp	x29, x30, [sp, -160]!
	add	x29, sp, 0
	ldp	x4, x5, [x0]
	fmov	d1, x4
	ldp	x2, x3, [x0, 32]
	fmov	d0, x2
	ins	v1.d[1], x5
	stp	x21, x22, [sp, 32]
	ldp	x6, x7, [x0, 16]
	ins	v0.d[1], x3
	ldp	x0, x1, [x0, 48]
	fmov	d2, x6
	adrp	x21, :got:__stack_chk_guard
	stp	x19, x20, [sp, 16]
	fmul	v1.4s, v0.4s, v1.4s
	fmov	d0, x0
	ins	v2.d[1], x7
	ldr	x8, [x21, #:got_lo12:__stack_chk_guard]
	ins	v0.d[1], x1
	adrp	x19, .LC2
	add	x20, x29, 112
	add	x19, x19, :lo12:.LC2
	ldr	x9, [x8]
	str	x9, [x29, 152]
	mov	x9,0
	stp	x4, x5, [x29, 48]
	stp	x6, x7, [x29, 64]
	mov	x22, 0
	fmul	v0.4s, v0.4s, v2.4s
	stp	x2, x3, [x29, 80]
	stp	x0, x1, [x29, 96]
	str	q1, [x29, 112]
	str	q0, [x29, 128]
	.p2align 3
.L2:
	ldr	s0, [x20, x22, lsl 2]
	mov	x1, x19
	mov	w0, 1
	add	x22, x22, 1
	fcvt	d0, s0
	bl	__printf_chk
	cmp	x22, 8
	bne	.L2
	ldr	x21, [x21, #:got_lo12:__stack_chk_guard]
	mov	w0, 0
	ldr	x2, [x29, 152]
	ldr	x1, [x21]
	eor	x1, x2, x1
	cbnz	x1, .L7
	ldp	x19, x20, [sp, 16]
	ldp	x21, x22, [sp, 32]
	ldp	x29, x30, [sp], 160
	ret
.L7:
	bl	__stack_chk_fail
	.size	main, .-main
	.section	.rodata
	.align	3
	.set	.LANCHOR0,. + 0
.LC0:
	.word	0
	.word	1065353216
	.word	1073741824
	.word	1077936128
	.word	1082130432
	.word	1084227584
	.word	1086324736
	.word	1088421888
.LC1:
	.word	1092616192
	.word	1093664768
	.word	1094713344
	.word	1095761920
	.word	1096810496
	.word	1097859072
	.word	1098907648
	.word	1099431936
	.section	.rodata.str1.8,"aMS",@progbits,1
	.align	3
.LC2:
	.string	"%8.3f\n"
	.ident	"GCC: (Ubuntu/Linaro 7.4.0-1ubuntu1~18.04.1) 7.4.0"
	.section	.note.GNU-stack,"",@progbits
```

先に「-O2」オプション(標準の最適化を行う)でコンパイルした例と比べるとだいぶ込み合っていて見づらいコードになっていますが、先の例にあったループ処理が展開されて上から下まで一直線なコードになっています。

あと、次の命令が新たに使用されているのがわかります。

```
    fmul	v1.4s, v0.4s, v1.4s
```
この命令をARMのサイトのオンラインマニュアルで調べると、

v0レジスタに入っている4個の単精度実数とv1レジスタに入っている
4個の単精度実数を乗算してv1レジスタにストアする

というベクトルタイプ(SIMD)の乗算命令だそうです。

NEONの命令のニモニックには頭に「v」がつきますが、AARCH64のSIMD命令はこのように、ニモニックはスカラの命令と同じで、オペランドの指定でベクトルの命令になるようです。

なお、コンパイル時オプションで「-march」を指定しなくても同じアセンブリコードが出力されましたので、「-O3」または「-Ofast」指定のみで自動的にSIMD命令が使えるところでSIMD命令を使ってくれるようです。

# NEON組み込み関数について

32ビットARM上のC言語の場合、「NEON組み込み関数」が使用できます。これはNEON命令を組み込み関数の形で明示的にC言語プログラムから使用できるものです。

NanoPi-NEO4上のCではどうでしょうか?

```neon_test.c
#include <stdio.h>
#include <arm_neon.h>

main()
{
	int i;

	float x[4] = { 1.0,2.0,3.0,4.0 };
	float y[4] = { 10.0,20.0,30.0,40.0 };
	float z[4];

	float32x4_t vx,vy,vz;

	vx = vld1q_f32(x);
	vy = vld1q_f32(y);
	vz = vmulq_f32(vx,vy);
	vst1q_f32(z,vz);

	for(i=0;i<4;i++)
	{
		printf("%7.2f\n",z[i]);
	}
}
```

先ほどのプログラムと同じく、単精度実数の配列の積を求めます。コンパイル結果のアセンブリコードは次のようになりました。

```neon_test.s
	.arch armv8-a
	.file	"neon_test.c"
	.text
	.section	.text.startup,"ax",@progbits
	.align	2
	.p2align 3,,7
	.global	main
	.type	main, %function
main:
	stp	x29, x30, [sp, -112]!
	adrp	x1, .LC0
	adrp	x0, .LC1
	add	x29, sp, 0
	ldr	q1, [x1, #:lo12:.LC0]
	stp	x21, x22, [sp, 32]
	ldr	q0, [x0, #:lo12:.LC1]
	adrp	x22, :got:__stack_chk_guard
	stp	x19, x20, [sp, 16]
	adrp	x21, .LC2
	ldr	x0, [x22, #:got_lo12:__stack_chk_guard]
	add	x20, x29, 112
	fmul	v2.4s, v0.4s, v1.4s
	add	x21, x21, :lo12:.LC2
	ldr	x1, [x0]
	str	x1, [x29, 104]
	mov	x1,0
	mov	x19, 0
	str	q1, [x29, 48]
	str	q0, [x29, 64]
	str	q2, [x20, -24]!
.L2:
	ldr	s0, [x20, x19, lsl 2]
	mov	x1, x21
	mov	w0, 1
	add	x19, x19, 1
	fcvt	d0, s0
	bl	__printf_chk
	cmp	x19, 4
	bne	.L2
	ldr	x22, [x22, #:got_lo12:__stack_chk_guard]
	mov	w0, 0
	ldr	x2, [x29, 104]
	ldr	x1, [x22]
	eor	x1, x2, x1
	cbnz	x1, .L7
	ldp	x19, x20, [sp, 16]
	ldp	x21, x22, [sp, 32]
	ldp	x29, x30, [sp], 112
	ret
.L7:
	bl	__stack_chk_fail
	.size	main, .-main
	.section	.rodata.cst16,"aM",@progbits,16
	.align	4
.LC0:
	.word	1065353216
	.word	1073741824
	.word	1077936128
	.word	1082130432
	.align	4
.LC1:
	.word	1092616192
	.word	1101004800
	.word	1106247680
	.word	1109393408
	.section	.text.startup
	.section	.rodata.str1.8,"aMS",@progbits,1
	.align	3
.LC2:
	.string	"%7.2f\n"
	.ident	"GCC: (Ubuntu/Linaro 7.4.0-1ubuntu1~18.04.1) 7.4.0"
	.section	.note.GNU-stack,"",@progbits
```

ここでも先の例と同じく、ベクトルタイプのfmul命令が使用されているのがわかります。組み込み関数の名前は「vmulq_f32()」で、これは同じ動作(単精度実数4個同士の乗算)をするNEON命令のニモニックに由来していますが、64ビットARM用のCコンパイラでコンパイルするとAARCH64のベクトルタイプfmul命令にそっくりそのまま置き換えられています。

# まとめ
64ビットARMプロセッサ上で「-mfpu=neon-fp-armv8なんてオプションねーよ」と怒られたときは「-march=armv8-a+simd」を指定してコンパイルしてみましょう。同様のSIMD命令を使用してコンパイルしてくれます。

また、-marchオプションを指定しなくても「-O3」「-Ofast」オプションのみの指定でも同じようにSIMD命令を使用してくれました。

NEON組み込み関数については、NEON命令に対応するベクトルタイプの演算命令が使用されます。

以上表題の答えでした。