LoginSignup
1
1

More than 3 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
1
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
1