FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス <a href="https://msyksphinz.github.io/github_pages/">

RISC-Vシミュレータの実装をLambda関数を使って簡単化してみる

GitHub上に公開している自作RISC-Vシミュレータは、命令フェッチ、命令デコーダ、命令実行の部分が分けて実装してある。

命令デコーダは500種類程度ある命令を1つに特定し、その情報に基づいて命令を実行するのだが、その命令の実装はそれぞれの命令で分割してある。 これは各命令で命令の動作を細かく制御できるようにするためだ。

f:id:msyksphinz:20180916025750p:plain

github.com

void InstEnv::RISCV_INST_LUI (InstWord_t inst_hex)
{
  RegAddr_t rd_addr = ExtractRDField (inst_hex);
  DWord_t   imm   = ExtendSign (ExtractBitField (inst_hex, 31, 12), 19);
  DWord_t   res   = m_pe_thread->SExtXlen (imm << 12);

  m_pe_thread->WriteGReg (rd_addr, res);
}


void InstEnv::RISCV_INST_AUIPC (InstWord_t inst_hex)
{
  RegAddr_t rd_addr = ExtractRDField (inst_hex);
  DWord_t   imm     = ExtendSign (ExtractBitField (inst_hex, 31, 12), 19);
  DWord_t   mask    = ~0xfff;
  UDWord_t  res   = ((imm << 12) & mask) + m_pe_thread->GetPC ();

  m_pe_thread->WriteGReg<UDWord_t> (rd_addr, m_pe_thread->SExtXlen(res));
}
...

こうすると問題は、この命令が実装してあるC++のファイルが増大しがちなのと、似たような命令に対して何度も同じような記述を繰り返さなければならないことだ。 例えば、add / sub / mul / divなどは演算子が異なるだけで、それ以外はすべて同じ実装となる。

これをすべて別々の命令実行関数として作るのはかなり骨が折れるし、ひとつ変えると全部の命令の仕様を変える必要があるため、大変だ。

そこで、整数命令(2R1W)、浮動小数点命令(2R1W)、浮動小数点(3R1W)などのテンプレートを用意して、それぞれの命令の細かい中身だけは分離して管理したい。 このため、各命令を記述した仕様書からコアの部分をラムダ式として記述し、記述を簡略化する方法を考えた。

命令の仕様

私の自作RISC-Vシミュレータの場合、命令の仕様は1つのRubyで記述された配列として記述してある。 命令の仕様書はCSV形式で書こうかと思ったが、記述の自由度とエディタで編集できることを考えると、スクリプト言語の一部として記述できた方がよいのかもしれない。

  • build/riscv_arch_table.rb
...
$arch_table.push(Array['add        r[11:7],r[19:15],r[24:20]'                                       , 32,  32,      '00000', '00',     'XXXXX', 'XXXXX', '000',    'XXXXX', '01100', '11', 'ALU',    "",           ["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 +  op2)"]])
$arch_table.push(Array['sub        r[11:7],r[19:15],r[24:20]'                                       , 32,  32,      '01000', '00',     'XXXXX', 'XXXXX', '000',    'XXXXX', '01100', '11', 'ALU',    "",           ["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 -  op2)"]])
...

命令デコードの仕様が最初に記述してあるが、必要なのは最後のこの部分だ。 この部分が、ラムダ関数で渡される。命令のテンプレートは、最初の3要素で決定される。

["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 +  op2)"]
["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 -  op2)"]

要素の1つ目がDestination Registerの型(この場合は64-bit Signed Registers)である。 また、2, 3番目の要素がSource Registerの型(64-bit Signed Registers)であることを示している。

このことから、このラムダ式を渡す関数はFunc_R_RR()であると決まる。以下のFunc_R_RR()関数のfunc()の部分が、ラムダ式に代わる。

  • src/riscv_inst_template.cpp
// Destination R, Source RR
template <typename Dst_t, typename Src_t, typename Func>
void RiscvPeThread::Func_R_RR (InstWord_t inst_hex, Func func)
{
  RegAddr_t rs1_addr = ExtractR1Field (inst_hex);
  RegAddr_t rs2_addr = ExtractR2Field (inst_hex);
  RegAddr_t rd_addr  = ExtractRDField (inst_hex);

  Src_t rs1_val = ReadGReg<Src_t> (rs1_addr);
  Src_t rs2_val = ReadGReg<Src_t> (rs2_addr);

  UWord_t fflags_dummy;
  Dst_t res     = func(rs1_val, rs2_val, 0, &fflags_dummy);   // ここの部分が、各命令で異なる部分。

  WriteGReg<Dst_t> (rd_addr, res);
}

そして、上記のRubyの仕様書から以下の関数を自動生成する。

  • src/inst_riscv__ALU.cpp
void InstEnv::RISCV_INST_ADD(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_RR<DWord_t, DWord_t> (inst_hex, [&](DWord_t op1, DWord_t op2, uint32_t round_mode, UWord_t *fflags) { return m_pe_thread->SExtXlen(op1 +  op2); });
}
void InstEnv::RISCV_INST_SUB(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_RR<DWord_t, DWord_t> (inst_hex, [&](DWord_t op1, DWord_t op2, uint32_t round_mode, UWord_t *fflags) { return m_pe_thread->SExtXlen(op1 -  op2); });
}

このテンプレートを活用することができれば、多くの算術演算は、演算の種類以外は共通のため、実際に記述しなければならないC++のコードの量を節約できる。

例えば、浮動小数点の関数群を以下のように記述した。

  • build/riscv_arch_table.rb
Array['fmadd.s    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatMadd (op1, op2, op3, fflags)"]]
Array['fmsub.s    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatMsub (op1, op2, op3, fflags)"]]
Array['fnmsub.s   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatNeg(InstOps::FloatMsub (op1, op2, op3, fflags))"]]
Array['fnmadd.s   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatNeg(InstOps::FloatMadd (op1, op2, op3, fflags))"]]
Array['fadd.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatAdd (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fsub.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatSub (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fmul.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatMul (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fdiv.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatDiv (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fsqrt.s    f[11:7],f[19:15]'                   ...     ["D", "D", "return InstOps::FloatSqrt (op1, softfloat_round_near_even, fflags)"]]
Array['fsgnj.s    f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "DWord_t c_op1 = ConvertNaNBoxing(op1); DWord_t c_op2 = ConvertNaNBoxing(op2); return (c_op1 & 0x7FFFFFFFULL) | ( c_op2          & 0x80000000ULL) | 0xffffffff00000000ULL"]]
Array['fsgnjn.s   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "DWord_t c_op1 = ConvertNaNBoxing(op1); DWord_t c_op2 = ConvertNaNBoxing(op2); return (c_op1 & 0x7FFFFFFFULL) | (~c_op2          & 0x80000000ULL) | 0xffffffff00000000ULL"]]
Array['fsgnjx.s   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "DWord_t c_op1 = ConvertNaNBoxing(op1); DWord_t c_op2 = ConvertNaNBoxing(op2); return (c_op1 & 0x7FFFFFFFULL) | ((c_op1 ^ c_op2) & 0x80000000ULL) | 0xffffffff00000000ULL"]]
Array['fmin.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatMin (op1, op2, fflags)"]]
Array['fmax.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatMax (op1, op2, fflags)"]]
Array['fcvt.w.s   r[11:7],f[19:15]'                   ...     ["RS", "D", "return InstOps::Convert_StoW  (op1, round_mode, fflags)"]]
Array['fcvt.wu.s  r[11:7],f[19:15]'                   ...     ["RS", "D", "return InstOps::Convert_StoWU (op1, round_mode, fflags)"]]
Array['fmv.x.w    r[11:7],f[19:15]'                   ...     ""])
Array['feq.s      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::FloatEq (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags)"]]
Array['flt.s      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::FloatLt (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags)"]]
Array['fle.s      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::FloatLe (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags)"]]
Array['fclass.s   f[11:7],f[19:15]'                   ...     ""])
Array['fcvt.s.w   f[11:7],r[19:15]'                   ...     ""])
Array['fcvt.s.wu  f[11:7],r[19:15]'                   ...     ""])
Array['fmv.w.x    f[11:7],r[19:15]'                   ...     ""])
Array['fld        f[11:7],r[19:15],h[31:20]'          ...     ""])
Array['fsd        f[24:20],h[31:25]|h[11:7](r[19:15])'...     ""])
Array['fmadd.d    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleMadd (op1, op2, op3, fflags)"]]
Array['fmsub.d    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleMsub (op1, op2, op3, fflags)"]]
Array['fnmsub.d   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleNeg(InstOps::DoubleMsub (op1, op2, op3, fflags))"]]
Array['fnmadd.d   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleNeg(InstOps::DoubleMadd (op1, op2, op3, fflags))"]]
Array['fadd.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleAdd (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fsub.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleSub (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fmul.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleMul (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fdiv.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleDiv (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fsqrt.d    f[11:7],f[19:15]'                   ...     ["D", "D", "return InstOps::DoubleSqrt (op1, softfloat_round_near_even, fflags)"]]
Array['fsgnj.d    f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return (op1 & 0x7FFFFFFFFFFFFFFFULL) | ( op2        & 0x8000000000000000ULL)"]]
Array['fsgnjn.d   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return (op1 & 0x7FFFFFFFFFFFFFFFULL) | (~op2        & 0x8000000000000000ULL)"]]
Array['fsgnjx.d   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return (op1 & 0x7FFFFFFFFFFFFFFFULL) | ((op1 ^ op2) & 0x8000000000000000ULL)"]]
Array['fmin.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleMin (op1, op2, fflags)"]]
Array['fmax.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleMax (op1, op2, fflags)"]]
Array['fcvt.s.d   f[11:7],f[19:15]'                   ...     ""])
Array['fcvt.d.s   f[11:7],f[19:15]'                   ...     ""])
Array['feq.d      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::DoubleEq (op1, op2, softfloat_round_near_even, fflags)"]]
Array['flt.d      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::DoubleLt (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fle.d      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::DoubleLe (op1, op2, softfloat_round_near_even, fflags)"]]

これらから、以下の命令の実装を自動生成できるようにした。

  • src/inst_riscv__FPU.cpp
void InstEnv::RISCV_INST_FMADD_S(InstWord_t inst_hex)
{
  m_pe_thread->Func_F_FFF<UDWord_t> (inst_hex, [](UDWord_t op1, UDWord_t op2, UDWord_t op3, UWord_t *fflags) { return InstOps::FloatMadd (op1, op2, op3, fflags); });
}
void InstEnv::RISCV_INST_FMSUB_S(InstWord_t inst_hex)
{
  m_pe_thread->Func_F_FFF<UDWord_t> (inst_hex, [](UDWord_t op1, UDWord_t op2, UDWord_t op3, UWord_t *fflags) { return InstOps::FloatMsub (op1, op2, op3, fflags); });
}
...
void InstEnv::RISCV_INST_FLT_D(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_FF<DWord_t, UDWord_t> (inst_hex, [](DWord_t op1, UDWord_t op2, uint32_t round_mode, UWord_t *fflags) { return InstOps::DoubleLt (op1, op2, softfloat_round_near_even, fflags); });
}
void InstEnv::RISCV_INST_FLE_D(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_FF<DWord_t, UDWord_t> (inst_hex, [](DWord_t op1, UDWord_t op2, uint32_t round_mode, UWord_t *fflags) { return InstOps::DoubleLe (op1, op2, softfloat_round_near_even, fflags); });
}

これで、自分で実装しなければならない命令の種類はかなり減るはずだ。