自作RISC-V命令セットシミュレータの実装続き。算術演算命令について説明すると、RISC-V Vector Extensionには大きく分けで3つの演算命令種が定義されている。
.VV
命令:ベクトルレジスタとベクトルレジスタ同士を算術演算して、その結果をベクトルレジスタに格納する。.VX
命令:ベクトルレジスタと汎用レジスタ同士を算術演算して、その結果をベクトルレジスタに格納する。.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
命令と同様に配列から値をロードして演算、結果を配列にストアして最後に一致比較、としている。以下のようなコードを命令毎に作ってひたすら流していった。
.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