FPGA開発日記

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

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

自作RISC-V命令セットシミュレータ、Vector Extensionのサポート続き。Unit Strideのメモリアクセス命令は追加したのだが、あと2種類のメモリアクセス命令についても追加する。それがStridedメモリアクセス命令とIndex Stridedメモリアクセス命令である。

復習するとRISC-V Vector Extensionには3種類のメモリアドレッシングモードが存在している。

  • Unit Stride メモリアドレッシング
  • Strided メモリアドレッシング
  • Indexed メモリアドレッシング

Unit Strideメモリアドレッシングについては普通のベクトルロード命令で前回一応実装した。次のStridedメモリアドレッシングだが、いわゆるScatter/Gatherのような命令で、とびとびの場所に対してメモリアクセスを発行することができる。疑似コードを書くとこんな感じである。引数strided(これは実際にはGPRからオペランドとして取られる)からストライドジャンプ距離が算出される。

// ロードの場合 (vlse8 / vlse16 / vlse32 ...)
for (int i = vstart; i < vl; i++) {
    vreg[i] = mem[base_addr + i * strided];
}
// ストアの場合 (vsse8 / vsse16 / vsee32 ...)
for (int i = vstart; i < vl; i++) {
    mem[base_addr + i * strided] = vreg[i];
}
f:id:msyksphinz:20200721010754p:plain:w500
Strided メモリアクセス

一方でIndexedメモリアドレッシングについては、各要素がメモリアクセスに使用するアドレスオフセットをベクトルレジスタから取得する。これがvoffsetレジスタから取得できるものとして疑似コードを書くと、

// ロードの場合 (vlxei8 / vlxei16 / vlxei32 ...)
for (int i = vstart; i < vl; i++) {
    vreg[i] = mem[base_addr + voffset[i]]
}
// ストアの場合 (vsxei8 / vsxei16 / vsxei32 )
for (int i = vstart; i < vl; i++) {
    mem[base_addr + voffset[i]] = vreg[i];
}

という感じである。さらに分類すると、IndexedにはOrderdとUnorderdに分類される。これはOrderdはメモリストアの順番を常に守らなければならず、Unorderdの方はメモリストアの順番を守らなくても良い。Orderedは例外が発生すると正確に失敗した要素の位置を特定できるが、Unorderedはそうとも限らないという制約がある。

f:id:msyksphinz:20200721010844p:plain:w500
Indexedメモリアクセス

という訳で、これを自作命令セットシミュレータに実装していく。ここではロード命令の実装のみ取り上げるが、

template <class T>
void RiscvPeThread::MemLoadStrided(const Addr_t mem_base_addr, const DWord_t rs2_val,
                                   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++) {
    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 * rs2_val);
    T  res;
    MemResult except = LoadFromBus (mem_addr, &res);
    CHECK_MEM_EXCEPTION(except, mem_addr);

    WriteVReg<T> (vd_addr, i, res);
  }
}
template <class T>
void RiscvPeThread::MemLoadIndexStrided(const Addr_t mem_base_addr,
                                        const RegAddr_t vs2_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++) {
    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;
      }
    }

    T index = ReadVReg<T> (vs2_addr, i);
    Addr_t mem_addr = mem_base_addr + index;
    T  res;
    MemResult except = LoadFromBus (mem_addr, &res);
    CHECK_MEM_EXCEPTION(except, mem_addr);

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

この実装をよく見ると気が付くかもしれないが、実は上記の疑似コードは正確ではなく、「オフセットの値をどのように取り扱うか」ということを十分に注意しなければならない。 仕様書によると、

Vector strided operations access the rst memory element at the base effective address, and then access subsequent elements at address increments given by the byte offset contained in the x register specified by rs2. Vector indexed operations add the contents of each element of the vector offset operand specified by vs2 to the base effective address to give the effective address of each element. The data vector register group has EEW=SEW, EMUL=LMUL, while the offset vector register group has EEW encoding in the instruction and EMUL=(EEW/SEW)*LMUL.

となっており、オフセットの値はどうも常に「バイト単位」であると考えられる。明記はされていないものの、例えば

li  t0, 2  # オフセットが2バイト単位で進んでいく。
vlse32.v v1,(a0),t0

上記のようなプログラムを組んだ場合、vlse32なので32ビット単位でデータをロードするのだが、オフセットを2バイト単位で進めるため必ずミスアラインが発生してしまうことになる。これはユーザ側が気をつけてコードを書かなければならない、と言うところになるのだろうか。