store命令 -scatterを添えて-


はじめに

$ grep ^vst[1234] /usr/lib/gcc/aarch64-linux-gnu/7.5.0/include/arm_neon.h  | cut -f 1 -d ' ' | sed -e 's/_[spfu][0-9]\+//g' | sort | uniq -c
     14 vst1
     14 vst1_lane
     14 vst1q
     14 vst1q_lane
     14 vst2
      1 vst2_lane_
     14 vst2q
      1 vst2q_lane_
     14 vst3
      1 vst3_lane_
     14 vst3q
      1 vst3q_lane_
     14 vst4
      1 vst4_lane_
     14 vst4q
      1 vst4q_lane_

vst1 (vstn)

  • load命令と対をなす。
  • vst1命令だとただメモリに保存する通常のstore命令
  • store命令はloadと同じくvst2vst3vst4まで存在する
  • loadではgatherを実現していたが、store命令ではscatterを実現する
st3q.cpp
        uint8_t data[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,};
        uint8_t dst[48] = { 255 };
        uint8x16x3_t vsrc;
        vsrc.val[0] = vld1q_u8(data     );
        vsrc.val[1] = vld1q_u8(data + 16);
        vsrc.val[2] = vld1q_u8(data + 32);
        vst3q_u8(dst , vsrc);
  • 演算結果
0       10      100     1       11      101     2       12      102     3       13      103     4       14      104     5
15      105     6       16      106     7       17      107     8       18      108     9       19      109     10      20
110     11      21      111     12      22      112     13      23      113     14      24      114     15      25      115
  • 見事に、vld3q命令の逆が行われ、010100から始まる数列がそれぞれ順番に並べられている様子が分かる
  • vld3q命令でロードしたベクトルに対して演算を行い、最後にvst3q命令で書き込めば、RGB画像において、各色独立の演算を施した上で同じフォーマットでメモリ上に書き戻せる
  • vld3qも神とたたえたが、このvst3q命令も神の如き尊さである。

vst1_lanevst1q_lane (vstn_lane)

  • このlaneがついた命令は、実際どう振る舞うのか
  • 動かして試してみよう
vst2q.cpp
        float data[] = { 1.0,  2.0,  3.0,  4.0, 10.0, 20.0, 30.0, 40.0};
        float res [] = { -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f};
        float32x4x2_t a;
        a.val[0] = vld1q_f32(data    );
        a.val[1] = vld1q_f32(data + 4);
        vst2q_lane_f32(res, a, 2);
  • 使い方はvld2q_lane命令同じで、vst2q_lane命令にはfloat32x4x2_t型のように、ベクトルを束ねた型を使う
    • 第3引数は0オリジンでレーン番号を指定する。
    • 当然範囲外を叩くとコンパイルエラーだし、数値はコンパイル時定数の必要がある。
  • 演算結果
3       30      -1      -1      -1      -1      -1      -1
  • 意外というかなんというか。
  • lane番号で指定した部分だけが書き出される。
  • なので、確保した配列res[8]のうち、後ろの6要素は触れられないままである

アライメントについて

  • SSEと違い、アライン(先頭アドレスが8 byteの倍数になっているか)を心配しなくてもHW側でよしなにやってくれる。1
  • ただし、Githubのissueにコメントしたが、32bit OS上でunalignedなアドレスから、u64s64のロードをすると、実行時エラーSIGBUSが起きる。
  • 解せないのは、同アドレスからvld1q_f32など、別の命令で適当にロードしても実行時エラーは起きず、ロードにvreinterpretq_xxで型を変えることが可能である
  • つまり「同じアドレス」から全く問題なくロードができる。
  • 解せないけれど、HWの仕様っぽいので割り切るしか無い。
チップの命令セット OSの32bit/64bit ベクトルレジスタの型 8byte alignment 結果
Armv8 64bit int32x4_t Y Success
Armv8 64bit int32x4_t N Success
Armv8 64bit int64x2_t Y Success
Armv8 64bit int64x2_t N Success
Armv8 32bit int32x4_t Y Success
Armv8 32bit int32x4_t N Success
Armv8 32bit int64x2_t Y Success
Armv8 32bit int64x2_t N Error (SIGBUS)
Armv7 32bit int32x4_t Y Success
Armv7 32bit int32x4_t N Success
Armv7 32bit int64x2_t Y Success
Armv7 32bit int64x2_t N Error (SIGBUS)
  • アセンブラレベルで見てみると、Arm v8 では当然違いは無い
  • Arm v7 のアセンブラを見てみると、
  • int64x2_tの場合(含むuint64x2_tint64x1_tuint64x1_t)
vld1.64 {d16-d17}, [r3:64]
  • それ以外の型の場合
vld1.32 {d16-d17}, [r3]
  • {d16-d17}は2本の64bitレジスタを指定している。
  • 命令のオペランドの[r3]は一般レジスタのr3に入ってるアドレスからロードする、という意味
  • 違いは
    • vld1.64vld1.32という末尾についてる要素の型
    • [r3]というアドレス指定が[r3:64]となっている
    • 多分、この:64が末尾についてることで8byte境界を期待しているのだと推測する
  • 何にしろそれ以外のロードに関してはアライメントの心配は要らない。

おわりに

  • store命令のvstとその派生形を紹介しました
  • 明日も手島の執筆の予定で、積和命令を紹介予定

  1. SSEでは16byte境界だが、NEONでは、前述の通りごく一部の命令に限って8byte境界の必要性がある