1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Quo vasistis, NEON instructiones?

Last updated at Posted at 2019-09-19

概要

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命令に対応するベクトルタイプの演算命令が使用されます。

以上表題の答えでした。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?