FPGA開発日記

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

RISC-V Vector命令をサポートした自作命令セットシミュレータの実装検討 (4. Unit Strideメモリアクセスの実装)

自作命令セットシミュレータのRISC-V Vector Extensionサポート、とりあえずCSR命令の確認が終わったので先に進めていく。

RISC-V Vector Extensionのメモリアクセスにはいくつか種類があって、大きく分けると3種類。

  • Unit Stride:最もシンプルなメモリアクセス。決まったサイズのまとまったブロックのメモリを扱う。
  • Strided:各レジスタに対して単一の距離を置いたメモリを扱う。
  • Indexed:各レジスタに対してばらばらの距離を置いたメモリを扱う。

に分けられる。

分かりにくいので図を使って説明するが、Unit Strideというのは一番単純なやつ。特定のメモリブロックに対して連続したメモリ領域を取り扱う。

f:id:msyksphinz:20200711103735p:plain:w500
Unit Stride Memory Access

Stridedというのはスカラレジスタにより指定された単一距離で分離されるメモリに対してアクセスを行う。たぶん世の中的にScatter/Gatherと呼ばれるやつ。

f:id:msyksphinz:20200711104514p:plain:w500
Strided Memory Access

IndexedというのはScatter/Gatherをさらに複雑にしたもので、ベクトルレジスタにより各エレメントに対するオフセットを独立に指定できる。もっとも複雑なパタン。

f:id:msyksphinz:20200711104139p:plain:w500
Indexed Memory Access

さらにメモリストアに関してはIndexed-UnorderdとIndex-Orderedに分かれている。Unorderedはメモリストアする要素の順番をばらばらに実行できる。これは実装最適化することを目的としているが、メモリアクセス時に正確な例外を検出することができないという問題がある。一方でIndex-Orderedはメモリストアする要素の順番は明確に決まっており、Unorderdに比べて性能は低下するが例外を正確に検出することができるというメリットがある。


で、これを自作命令セットシミュレータにどのように実装するかという話で、まずはベクトルレジスタを定義しなくてはならない。とりあえず8ビットよりも小さなSEWになる事は無いだろうということで、最小要素を8ビットとして8xVLENBx32のサイズのベクトルレジスタを用意する。

class RiscvPeThread : public EnvBase
{
 private:
  RiscvDec *m_ptr_riscv_dec;
  std::unique_ptr<DWord_t []> m_regs;   // general register
  std::unique_ptr<DWord_t []> m_fregs;  // floating point registers
  std::unique_ptr<uint8_t []> m_vregs;  // Vector Registers
  std::unique_ptr<CsrEnv>  m_csr_env;   // CSR system register information
/*!
 * create new RISCV simulation environment
 * \return RiscvPeThread structure (not formatted)
 */
RiscvPeThread::RiscvPeThread (FILE           *dbgfp,
                              RiscvBitMode_t bit_mode,
                              uint64_t       misa,
                              PrivMode       maxpriv,
                              bool           en_stop_host,
                              bool           is_debug_trace,
                              FILE           *uart_fp,
                              bool           trace_hier,
                              std::string    trace_out,
                              bool           is_misa_writable)
    : EnvBase (is_debug_trace, dbgfp, trace_hier, trace_out),
      m_bit_mode (bit_mode),
      m_en_stop_host (en_stop_host),
      m_is_misa_writable (is_misa_writable)
{
...
  m_vregs = std::unique_ptr<uint8_t []>(new uint8_t[get_VLENB() * 32]);

このレジスタにアクセスるためには、以下の3つの要素を指定する必要がある。

  • アクセスするデータ型
  • レジスタ番号
  • レジスタの要素番号

ということで、データ型についてはtempalteでパラメータ化し、それ以外の部分は引数として取り扱うことにした。

template <class T>
T RiscvPeThread::ReadVReg (RegAddr_t reg_idx, uint32_t elem_idx)
{
  T value = *((reinterpret_cast<T*>(m_vregs.get())) + reg_idx * get_VLENB() + elem_idx);
  return value;
}

template <class T>
void RiscvPeThread::WriteVReg (RegAddr_t reg_idx, uint32_t elem_idx, T data)
{
  *((reinterpret_cast<T*>(m_vregs.get())) + reg_idx * get_VLENB() + elem_idx) = data;
}

reinterpret_castとか.get()とか多用しているけど大丈夫かな... 自分で作っておいて少し心配だ...

メモリアクセスは既存の関数を使用して、まずは一般的なUnit Strideのメモリアクセスルーチンを作る。これはデータ型の大きさに関わらない一般的な共通ルーチンとする。LMULを省略しているがvlの中に入っているので大丈夫のはずだ。

template <class T>
void RiscvPeThread::MemLoadUnitStride(Addr_t mem_base_addr,
                                      const RegAddr_t rs1_addr, const RegAddr_t vd_addr,
                                      bool vm, T type) {
  const int DWIDTH = sizeof(T) * 8;
  Word_t vl;
  CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VL), &vl);
  Word_t vstart; CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VSTART), &vstart);
  for (int i = vstart; i < vl; i++) {
    // int elem_position_byte = i * sizeof(T) * 8;
    if (vm == 0) {
      const int midx = i / DWIDTH;
      const int mpos = i % DWIDTH;
      bool skip = ((ReadVReg<T>(0, midx) >> mpos) & 0x1) == 0;
      if (skip) {
        continue;
      }
    }

    Addr_t mem_addr = mem_base_addr + i * DWIDTH / 8;
    T  res;
    MemResult except = LoadFromBus (mem_addr, &res);
    CHECK_MEM_EXCEPTION(except, mem_addr);

    WriteVReg<T> (vd_addr, i, res);
  }
}

CHECK_MEM_EXCEPTIONについてはあまりにも煩雑なのでマクロでまとめた。

#define CHECK_MEM_EXCEPTION(cause, addr) \
    if (cause == MemResult::MemMisAlign) { \
      CSRWrite (static_cast<Addr_t>(SYSREG_ADDR_VSTART), i); \
      GenerateException (ExceptCode::Except_LoadAddrMisalign, addr); \
      return; \
    } \
    if (cause == MemResult::MemTlbError) { \
      CSRWrite (static_cast<Addr_t>(SYSREG_ADDR_VSTART), i); \
      GenerateException (ExceptCode::Except_LoadPageFault, addr); \
      return; \
    } \
    if (cause == MemResult::MemNotDefined) { \
      CSRWrite (static_cast<Addr_t>(SYSREG_ADDR_VSTART), i); \
      GenerateException (ExceptCode::Except_LoadAccessFault, addr); \
      return; \
    } \

vle8.vから順にこんな感じで実装していく。ちなみにMemLoadUnitStride()の最後に変な引数0が付いているのは、これを付けないと8ビット、16ビット、32ビットなどですべて引数の形が同じになってしまい、template化できなくなったため。仕方がないのでDummyの型が異なる引数を1つ挿入した。これ、何とかならんかな。ってかだからSpikeの実装はすべてマクロで書いてあるのかな。

void InstEnv::RISCV_INST_VLE8_V(InstWord_t inst_hex)
{
  if (!m_pe_thread->IsVECAvailable ()) {
    m_pe_thread->GenerateException (ExceptCode::Except_IllegalInst, 0);
    return;
  }

  const RegAddr_t rs1_addr = ExtractR1Field (inst_hex);
  const RegAddr_t vd_addr  = ExtractRDField (inst_hex);
  bool vm = ExtractBitField(inst_hex, 25, 25);
  Addr_t mem_base_addr  = m_pe_thread->ReadGReg<DWord_t> (rs1_addr);

  m_pe_thread->MemLoadUnitStride<Byte_t>(mem_base_addr, rs1_addr, vd_addr, vm, static_cast<Byte_t>(0));
}

テストを動かそう。以下のようなプログラムを作ってみた。単純なmemcpyだ。16ビット版と32ビット版、64ビット版も要してテストしてみる。

    .text
    .global     copy_data_vec
# void *memcpy(void* dest, const void* src, size_t n)
# a0=dest, a1=src, a2=n
#
copy_data_vec:
    mv      a3, a0          # Copy destination
_loop:
    vsetvli t0, a2, e8,m1   # Vectors of 8b
    vle8.v  v0, (a1)        # Load bytes
    add     a1, a1, t0      # Bump pointer
    sub     a2, a2, t0      # Decrement count
    vse8.v  v0, (a3)     # Store bytes
    add     a3, a3, t0      # Bump pointer
    bnez    a2, _loop      # Any more?
    ret                  # Return

チェック関数はC言語で書いた。sourceとdestinationが同一であればコピーができているとする。

int32_t dest_data[DATA_NUM];

int check_data (int32_t *vec_data, const int32_t *scalar_data, const int data_num)
{
  for(int i = 0; i < data_num; i++) {
    if(vec_data[i] != scalar_data[i]) {
      return i + 1;
    }
  }
  return 0;
}


int main()
{
  copy_data_vec(dest_data, source_data, DATA_NUM * sizeof(int32_t) / sizeof(int8_t));
  return check_data(dest_data, source_data, DATA_NUM);
}

テストを動かす。ログが大変なことになった。Spikeを確認したが同様にログが汚い。もう少し何とかならんものか。

f:id:msyksphinz:20200711110432p:plain