自作命令セットシミュレータのRISC-V Vector Extensionサポート、とりあえずCSR命令の確認が終わったので先に進めていく。
RISC-V Vector Extensionのメモリアクセスにはいくつか種類があって、大きく分けると3種類。
- Unit Stride:最もシンプルなメモリアクセス。決まったサイズのまとまったブロックのメモリを扱う。
- Strided:各レジスタに対して単一の距離を置いたメモリを扱う。
- Indexed:各レジスタに対してばらばらの距離を置いたメモリを扱う。
に分けられる。
分かりにくいので図を使って説明するが、Unit Strideというのは一番単純なやつ。特定のメモリブロックに対して連続したメモリ領域を取り扱う。
Stridedというのはスカラレジスタにより指定された単一距離で分離されるメモリに対してアクセスを行う。たぶん世の中的にScatter/Gatherと呼ばれるやつ。
IndexedというのはScatter/Gatherをさらに複雑にしたもので、ベクトルレジスタにより各エレメントに対するオフセットを独立に指定できる。もっとも複雑なパタン。
さらにメモリストアに関しては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を確認したが同様にログが汚い。もう少し何とかならんものか。