概要
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命令によるベクトル化の例として、次のプログラムをコンパイルしてみることにします。
#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」を指定します。
.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
今回はどんなアセンブリコードが出力されたでしょうか。味わってみましょう。
.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ではどうでしょうか?
#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]);
}
}
先ほどのプログラムと同じく、単精度実数の配列の積を求めます。コンパイル結果のアセンブリコードは次のようになりました。
.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命令に対応するベクトルタイプの演算命令が使用されます。
以上表題の答えでした。