FPGA開発日記

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

RISC-V Vector命令をサポートした自作命令セットシミュレータの実装検討 (8. VX, VI命令の実装)

自作RISC-V命令セットシミュレータの実装続き。算術演算命令について説明すると、RISC-V Vector Extensionには大きく分けで3つの演算命令種が定義されている。

  • .VV 命令:ベクトルレジスタとベクトルレジスタ同士を算術演算して、その結果をベクトルレジスタに格納する。
  • .VX 命令:ベクトルレジスタと汎用レジスタ同士を算術演算して、その結果をベクトルレジスタに格納する。
  • .VI 命令:ベクトルレジスタの即値同士を算術演算して、その結果をベクトルレジスタに格納する。
f:id:msyksphinz:20200726120943p:plain:w500
.VV命令系統の概要
f:id:msyksphinz:20200726121008p:plain:w500
.VX命令系統の概要
f:id:msyksphinz:20200726121035p:plain:w500
.VI命令系統の概要

この時に少しややこしいのが、.VX命令と.VI命令についてよくよく見てみると、命令定義では、

  • オペランド1:汎用レジスタ・即値
  • オペランド2:ベクトルレジスタ

となっており直感と逆である。しかもオペランドの並びは、オペランド2、オペランド1という順番になっている。演算の順序もvs2 op vs1となっている。

vop.vv  vd, vs2, vs1, vm  # integer vector-vector      vd[i] = vs2[i] op vs1[i] 
vop.vx  vd, vs2, rs1, vm  # integer vector-scalar      vd[i] = vs2[i] op x[rs1] 
vop.vi  vd, vs2, imm, vm  # integer vector-immediate   vd[i] = vs2[i] op imm 

この順序さえ間違わなければテストを作るときに混乱せずにすむ。私は混乱した。

で、これらの.VX命令、.VI命令の実装だが、これも.VV命令と同様にテンプレートを用意して命令の実装方法を統一した。

void InstEnv::RISCV_INST_VADD_VX  (InstWord_t inst_hex) { VEC_VX_LOOP(op2 + op1);               }
void InstEnv::RISCV_INST_VSUB_VX  (InstWord_t inst_hex) { VEC_VX_LOOP(op2 - op1);               }
void InstEnv::RISCV_INST_VRSUB_VX (InstWord_t inst_hex) { VEC_VX_LOOP(op1 - op2);               }
void InstEnv::RISCV_INST_VMINU_VX (InstWord_t inst_hex) { VEC_VX_LOOP_U(op1 < op2 ? op1 : op2); }
void InstEnv::RISCV_INST_VMIN_VX  (InstWord_t inst_hex) { VEC_VX_LOOP(op1 < op2 ? op1 : op2);   }
void InstEnv::RISCV_INST_VMAXU_VX (InstWord_t inst_hex) { VEC_VX_LOOP_U(op1 > op2 ? op1 : op2); }
void InstEnv::RISCV_INST_VMAX_VX  (InstWord_t inst_hex) { VEC_VX_LOOP(op1 > op2 ? op1 : op2);   }
void InstEnv::RISCV_INST_VAND_VX  (InstWord_t inst_hex) { VEC_VX_LOOP(op2 & op1);               }
void InstEnv::RISCV_INST_VOR_VX   (InstWord_t inst_hex) { VEC_VX_LOOP(op2 | op1);               }
void InstEnv::RISCV_INST_VXOR_VX  (InstWord_t inst_hex) { VEC_VX_LOOP(op2 ^ op1);               }
...
void InstEnv::RISCV_INST_VADD_VI  (InstWord_t inst_hex) { VEC_VI_LOOP(op2 + op1); }
void InstEnv::RISCV_INST_VRSUB_VI (InstWord_t inst_hex) { VEC_VI_LOOP(op1 - op2); }
void InstEnv::RISCV_INST_VAND_VI  (InstWord_t inst_hex) { VEC_VI_LOOP(op2 & op1); }
void InstEnv::RISCV_INST_VOR_VI   (InstWord_t inst_hex) { VEC_VI_LOOP(op2 | op1); }
void InstEnv::RISCV_INST_VXOR_VI  (InstWord_t inst_hex) { VEC_VI_LOOP(op2 ^ op1); }

VEC_VX_LOOPは、VEC_VV_LOOPと非常に似ている。オペランド2を汎用レジスタ読み込みに変えているだけである。そういう意味ではもっと簡略化できそうかな。

#define VEC_VX_LOOP(op)                                                                            \
  REQUIRE_VEC;                                                                                    \
                                                                                                  \
  RegAddr_t vs1_addr = ExtractR1Field (inst_hex);                                                 \
  RegAddr_t vs2_addr = ExtractR2Field (inst_hex);                                                 \
  RegAddr_t vd_addr  = ExtractRDField (inst_hex);                                                 \
                                                                                                  \
  bool vm = ExtractBitField(inst_hex, 25, 25);                                                    \
                                                                                                  \
  int vtype;                                                                                      \
  m_pe_thread->CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VTYPE), &vtype);                          \
  RvvSEW sew = static_cast<RvvSEW>((vtype >> 2) & 0x7);                                           \
                                                                                                  \
  switch(sew) {                                                                                   \
    case RvvSEW::SEW_8BIT:                                                                        \
      m_pe_thread->VecExecIntVX2Op<Byte_t, Byte_t> (vm, vs1_addr, vs2_addr,                         \
                                                    vd_addr,                                        \
                                                    [](Byte_t op1, Byte_t op2) { return op; });     \
      break;                                                                                      \
    case RvvSEW::SEW_16BIT:                                                                       \
      m_pe_thread->VecExecIntVX2Op<HWord_t, HWord_t> (vm, vs1_addr, vs2_addr,                       \
                                                      vd_addr,                                      \
                                                      [](HWord_t op1, HWord_t op2) { return op; }); \
      break;                                                                                      \
    case RvvSEW::SEW_32BIT:                                                                       \
      m_pe_thread->VecExecIntVX2Op<Word_t, Word_t> (vm, vs1_addr, vs2_addr,                         \
                                                    vd_addr,                                        \
                                                    [](Word_t op1, Word_t op2) { return op; });     \
      break;                                                                                      \
    case RvvSEW::SEW_64BIT:                                                                       \
      m_pe_thread->VecExecIntVX2Op<DWord_t, DWord_t> (vm, vs1_addr, vs2_addr,                       \
                                                      vd_addr,                                      \
                                                      [](DWord_t op1, DWord_t op2) { return op; }); \
      break;                                                                                      \
    default:                                                                                      \
      m_pe_thread->DebugPrint("Unsupported SEW.");                                                \
      m_pe_thread->GenerateException (ExceptCode::Except_IllegalInst, 0);                         \
      return;                                                                                     \
  }                                                                                               \
                                                                                                  \
  return;

テストは、.VV命令と同様に配列から値をロードして演算、結果を配列にストアして最後に一致比較、としている。以下のようなコードを命令毎に作ってひたすら流していった。

github.com

    .text
    .global     add_data_vec_8
# void add_data_vec_8(int8_t *dest_data, int8_t src1, int8_t *src2, int data_num);
# a0=dest, a1=src1, a2=src2, a3=n
#
add_data_vec_8:
    // mv      a3, a0          # Copy destination
.loop_8:
    vsetvli t0, a2, e8,m1   # Vectors of 8b
    vle8.v  v1, (a2)        # Load bytes
    add     a2, a2, t0      # Bump pointer
    sub     a3, a3, t0      # Decrement count
    vadd.vx v2, v1, a1      # Vector Add
    vse8.v  v2, (a0)     # Store bytes
    add     a0, a0, t0      # Bump pointer
    bnez    a3, .loop_8     # Any more?
    ret                  # Return