FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V Vector intrinsicのサンプルアプリケーションを解析する

RISC-V Vector Intrinsicのドキュメントでは、サンプルアプリケーションがいくつか格納されている。

https://github.com/riscv-non-isa/rvv-intrinsic-doc/tree/master/examples

それぞれのアプリケーションにおいて、どのようにベクトル命令が使用されているのかを見てみることにした。

rvv_strcpy

char *strcpy_vec(char *dst, const char *src) {
  char *save = dst;
  size_t vlmax = vsetvlmax_e8m8();
  long first_set_bit = -1;
  size_t vl;
  while (first_set_bit < 0) {
    vint8m8_t vec_src = vle8ff_v_i8m8(src, &vl, vlmax);

    vbool1_t string_terminate = vmseq_vx_i8m8_b1(vec_src, 0, vl);
    vbool1_t mask = vmsif_m_b1(string_terminate, vl);

    vse8_v_i8m8_m(mask, dst, vec_src, vl);

    src += vl;
    dst += vl;

    first_set_bit = vfirst_m_b1(string_terminate, vl);
  }
  return save;
}
   10184:   041076d7            vsetvli a3,zero,e8,m2,ta,mu
   10188:   03070d07            vle8ff.v    v26,(a4)
   1018c:   03078e07            vle8ff.v    v28,(a5)
   10190:   63a03c57            vmseq.vi    v24,v26,0
   10194:   67ae0d57            vmsne.vv    v26,v26,v28
   10198:   6b8d2c57            vmor.mm v24,v24,v26
   1019c:   4388a657            vfirst.m    a2,v24
   101a0:   c20026f3            csrr    a3,vl
   101a4:   fc064ee3            bltz    a2,10180 <main+0x7a>
  1. vsetvliレジスタ幅を最大に設定して読み込みデータ量を最大にする。

  2. vle8ff.vで文字列をロードする。ffを使用しているのは、アクセス禁止領域まではみ出した場合の処理を考慮するため。

  3. vmseq.vi 0により、最初に0(つまり文字列の最後)である場所にマークを付ける。
  4. vmsif.mにより、マークを付けたレジスタの下位のビットをすべて1に設定する(文字列の途中だと、基本的にすべて1になるはず)。この結果をv0に格納する。
  5. vse8.vにより、マークを付けた場所のみストアする
  6. 処理したバイト数だけポインタを進め、残り文字数countを減算する
  7. vfirst.mにより、文字列の最後が検出されたかどうかをチェックする。文字列の最後が検出された場合、ループを抜ける。

rvv_strncpy

char *strncpy_vec(char *dst, char *src, size_t count) {
  char *save = dst;
  size_t new_vl;
  long first_set_bit = -1;

  while (first_set_bit < 0) {
    if (count == 0)
      return save;
    size_t vl = vsetvl_e8m1(count);

    vint8m1_t vec_src = vle8ff_v_i8m1(src, &new_vl, vl);

    vbool8_t string_terminate = vmseq_vx_i8m1_b8(vec_src, 0, new_vl);
    vbool8_t mask = vmsif_m_b8(string_terminate, new_vl);

    vse8_v_i8m1_m(mask, dst, vec_src, new_vl);

    count -= new_vl;
    src += new_vl;
    dst += new_vl;

    first_set_bit = vfirst_m_b8(string_terminate, new_vl);
  }

  size_t tail = new_vl - first_set_bit;
  count += tail;
  dst -= tail;
  size_t vlmax = vsetvlmax_e8m1();
  vint8m1_t vec_zero = vmv_v_x_i8m1(0, vlmax);
  do {
    size_t vl = vsetvl_e8m1(count);
    vse8_v_i8m1(dst, vec_zero, vl);
    count -= vl;
    dst += vl;
  } while (count > 0);

  return save;
}
   1017e:   0c37f057            vsetvli zero,a5,e8,m8,ta,ma
   10182:   03058c07            vle8ff.v    v24,(a1)
   10186:   63803457            vmseq.vi    v8,v24,0
   1018a:   5281a057            vmsif.m v0,v8
   1018e:   00070c27            vse8.v  v24,(a4),v0.t
   10192:   c2002673            csrr    a2,vl
   10196:   4288a557            vfirst.m    a0,v8

基本的にrvv_strcpyと似ているが、countよりもコピーした文字列が小さかった場合、残りを\0で埋める。

rvv_strcmp

int strcmp_vec(const char *src1, const char *src2) {
  size_t vlmax = vsetvlmax_e8m2();
  long first_set_bit = -1;
  size_t vl, vl1;
  while (first_set_bit < 0) {
    vint8m2_t vec_src1 = vle8ff_v_i8m2(src1, &vl, vlmax);
    vint8m2_t vec_src2 = vle8ff_v_i8m2(src2, &vl1, vlmax);

    vbool4_t string_terminate = vmseq_vx_i8m2_b4(vec_src1, 0, vl);
    vbool4_t no_equal = vmsne_vv_i8m2_b4(vec_src1, vec_src2, vl);
    vbool4_t vec_terminate = vmor_mm_b4(string_terminate, no_equal, vl);

    first_set_bit = vfirst_m_b4(vec_terminate, vl);
    src1 += vl;
    src2 += vl;
  }
  src1 -= vl - first_set_bit;
  src2 -= vl - first_set_bit;
  return *src1 - *src2;
}
   106ae:   04107657            vsetvli a2,zero,e8,m2,ta,mu
   106b2:   04167057            vsetvli zero,a2,e8,m2,ta,mu
   106b6:   03050c07            vle8ff.v    v24,(a0)
   106ba:   04167057            vsetvli zero,a2,e8,m2,ta,mu
   106be:   03058e07            vle8ff.v    v28,(a1)
   106c2:   c20027f3            csrr    a5,vl
   106c6:   88aa                    mv  a7,a0
   106c8:   0417f057            vsetvli zero,a5,e8,m2,ta,mu
   106cc:   882e                    mv  a6,a1
   106ce:   63803d57            vmseq.vi    v26,v24,0
   106d2:   953e                    add a0,a0,a5
   106d4:   678e0c57            vmsne.vv    v24,v24,v28
   106d8:   95be                    add a1,a1,a5
   106da:   6bac2c57            vmor.mm v24,v26,v24
   106de:   4388a6d7            vfirst.m    a3,v24
   106e2:   fc06c8e3            bltz    a3,106b2 <strcmp_vec+0x4>

srcとdstを比較する。

  1. vle8ff.vにより、v24/v25に文字列をロードする
  2. vle8ff.vにより、v28/v29に文字列をロードする
  3. vmseq.viにより、v24/v25で0になっている場所をマークし、v26に格納する (これが文字列の終端のマークになる)
  4. vmsne.vにより、ロードした文字列同士を比較して結果をv24に格納する
  5. vmor.mmにより、マーク同士をORし、文字列の最終か、不一致の場所をマークする
  6. マークした場所に基づいてvfirst.mを用いてインデックスを計算する

rvv_branch

void branch(double *a, double *b, double *c, int n, double constant) {
  // set vlmax and initialize variables
  size_t vlmax = vsetvlmax_e64m1();
  vfloat64m1_t vec_zero = vfmv_v_f_f64m1(0, vlmax);
  vfloat64m1_t vec_constant = vfmv_v_f_f64m1(constant, vlmax);
  for (size_t vl; n > 0; n -= vl, a += vl, b += vl, c += vl) {
    vl = vsetvl_e64m1(n);

    vfloat64m1_t vec_a = vle64_v_f64m1(a, vl);
    vfloat64m1_t vec_b = vle64_v_f64m1(b, vl);

    vbool64_t mask = vmfne_vv_f64m1_b64(vec_b, vec_zero, vl);

    vfloat64m1_t vec_c =
        vfdiv_vv_f64m1_m(mask, /*maskedoff=*/vec_constant, vec_a, vec_b, vl);
    vse64_v_f64m1(c, vec_c, vl);
  }
}

倍精度浮動小数点配列abの値をロードし、bの値が0でない要素について、要素aと要素bの値をそれぞれ除算し配列cに格納する。bの値が0の場合には、cconstantの値を代わりに格納する。

   10168:   05807757            vsetvli a4,zero,e64,m1,ta,mu
   1016c:   5e075dd7            vfmv.v.f    v27,fa4
   10170:   0587f8d7            vsetvli a7,a5,e64,m1,ta,mu
   10174:   9fb03cd7            vmv1r.v v25,v27
   10178:   02087c07            vle64.v v24,(a6)
   1017c:   411787b3            sub a5,a5,a7
   10180:   7387d057            vmfne.vf    v0,v24,fa5
   10184:   00057d07            vle64.v v26,(a0),v0.t
   10188:   81ac1cd7            vfdiv.vv    v25,v26,v24,v0.t
   1018c:   0206fca7            vse64.v v25,(a3)
   10190:   982e                    add a6,a6,a1
   10192:   952e                    add a0,a0,a1
   10194:   96ae                    add a3,a3,a1
   10196:   ffe9                    bnez    a5,10170 <main+0x6a>
  1. vsetvliにより演算対象を64ビットに設定する
  2. vfmv.v.fにより、浮動小数点引数constantの値をベクトルレジスタに展開する
  3. ループ開始
  4. vsetvliにより、最大ベクトル長を設定する
  5. vle64.vによりベクトル要素bをロードする
  6. ベクトル要素bの値に基づいてマスクを作成する。要素が0でない場合にマスクを設定する
  7. vle64.vによりベクトル要素aをロードする(最適化により、aのロードする要素はマスクの情報に基づいてロードしている)
  8. 除算命令(vfdiv.vv)の書き込みレジスタには、あらかじめv27(constant)の値がロードしてあり、演算が行われる場合は除算の結果が書き込まれ、マスクされている場合はデフォルト値(constant)が維持されるようになっている。
  9. これをすべての要素に対して繰り返す

rvv_index

void index_(double *a, double *b, double *c, int n) {
  size_t vlmax = vsetvlmax_e32m1();
  vuint32m1_t vec_i = vid_v_u32m1(vlmax);
  for (size_t vl; n > 0; n -= vl, a += vl, b += vl, c += vl) {
    vl = vsetvl_e64m2(n);

    vfloat64m2_t vec_i_double = vfwcvt_f_xu_v_f64m2(vec_i, vl);

    vfloat64m2_t vec_b = vle64_v_f64m2(b, vl);
    vfloat64m2_t vec_c = vle64_v_f64m2(c, vl);

    vfloat64m2_t vec_a =
        vfadd_vv_f64m2(vec_b, vfmul_vv_f64m2(vec_c, vec_i_double, vl), vl);
    vse64_v_f64m2(a, vec_a, vl);

    vec_i = vadd_vx_u32m1(vec_i, vl, vl);
  }
}

倍精度浮動小数点の要素b/cおよび整数nを用いて以下の演算を行う。

$a[] = b[] + c[*] \times (double)c$

   10188:   050077d7            vsetvli a5,zero,e32,m1,ta,mu
   1018c:   5208ae57            vid.v   v28
   10190:   0596f7d7            vsetvli a5,a3,e64,m2,ta,mu
   10194:   0507f057            vsetvli zero,a5,e32,m1,ta,mu
   10198:   0007831b            sext.w  t1,a5
   1019c:   02087c07            vle64.v v24,(a6)
   101a0:   0208fd07            vle64.v v26,(a7)
   101a4:   00379513            slli    a0,a5,0x3
   101a8:   4bc51f57            vfwcvt.f.xu.v   v30,v28
   101ac:   406686bb            subw    a3,a3,t1
   101b0:   05907057            vsetvli zero,zero,e64,m2,ta,mu
   101b4:   93af1d57            vfmul.vv    v26,v26,v30
   101b8:   038d1c57            vfadd.vv    v24,v24,v26
   101bc:   0205fc27            vse64.v v24,(a1)
   101c0:   982a                    add a6,a6,a0
   101c2:   05007057            vsetvli zero,zero,e32,m1,ta,mu
   101c6:   95aa                    add a1,a1,a0
   101c8:   98aa                    add a7,a7,a0
   101ca:   03c34e57            vadd.vx v28,v28,t1
   101ce:   fcd041e3            bgtz    a3,10190 <main+0x8a>

整数は32ビットで浮動小数点は64ビットなので、型の変換とレジスタの拡張に注意。

  1. vid.vによりv28に32ビットのシーケンシャルな値を展開する
  2. vsetvliにより演算の型情報を32ビットに設定している。
  3. その後、vle64.vを実行してaの値をロードしているが、VSEWが32ビットなので、レジスタ2つ分(v24/v25)に値がロードされる
  4. 同様にvle64.vにより、v26,v27にbの値がロードされる
  5. vfwcvt.f.xu.vにより、v28の32ビット毎のシーケンシャルの値が、v30,v31の2つのレジスタに倍精度の浮動小数点として返還される
  6. vfmul.vvによりv26/v27の値とv30/v31の値の乗算が行われる
  7. vfadd.vvにより上記の演算結果とv24/v25の結果を加算する
  8. vse64.vにより、v24/v25の値をストアする
  9. vsetvliにより、e32/m1でレジスタ1本の使用モードに変換する
  10. vadd.vxにより、vid.vで指定したシーケンシャルな値をそれぞれ次の配列に備えてインクリメントする

rvv_saxpy

void saxpy_vec(size_t n, const float a, const float *x, float *y) {
  size_t l;

  vfloat32m8_t vx, vy;

  for (; n > 0; n -= l) {
    l = vsetvl_e32m8(n);
    vx = vle32_v_f32m8(x, l);
    x += l;
    vy = vle32_v_f32m8(y, l);
    vy = vfmacc_vf_f32m8(vy, a, vx, l);
    vse32_v_f32m8 (y, vy, l);
    y += l;
  }
}

シンプルに配列xと配列y、定数aを使用して以下の演算を行う。

$y[] = y[] + x[*] * a$

   1015a:   053777d7            vsetvli a5,a4,e32,m8,ta,mu
   1015e:   0205e407            vle32.v v8,(a1)
   10162:   0206ec07            vle32.v v24,(a3)
   10166:   00279513            slli    a0,a5,0x2
   1016a:   0137f057            vsetvli zero,a5,e32,m8,tu,mu
   1016e:   8f1d                    sub a4,a4,a5
   10170:   b287dc57            vfmacc.vf   v24,fa5,v8
   10174:   0206ec27            vse32.v v24,(a3)
   10178:   95aa                    add a1,a1,a0
   1017a:   96aa                    add a3,a3,a0
   1017c:   ff79                    bnez    a4,1015a <main+0x54>
  1. vsetvliによりVSEW=32, 8レジスタを使用するモードにする
  2. vle32.vにより、配列xの値をv8-v15にロードする
  3. vle32.vにより、配列yの値をv24-v31にロードする
  4. vfmacc.vfにより、v8-v15, v24-v31とfa5を用いて演算を行う
  5. 演算結果をvse32.vでストアする