公開日:2026/01/31
title.png

37th Season


#37-10 Nihoniumパイプライン実装(Me)

 Cmod A7ボード(FPGA)を使って開発しているオリジナル32ビットCPUコアNihoniumの実装方式として、パイプライン方式を採用してみたいと思います。

 前回、Nihoniumを4段パイプライン方式で実装するため、各ステージ毎の処理内容を明確にして、ブロック図を作成しました。各ステージは同時並列に処理を実行するので、共有資源の競合などについて検討を広げます。

資源の競合

 CPUが扱う資源としては、汎用レジスタや内部バス、算術論理演算ユニットALU、外部バス(メモリバス/フェッチバス)など、が挙げられます。資源が競合しない様にステージを分割するのが基本なのですが、それぞれに他の資源と関係しあうので、完全に競合を避ける事はできません。

 もう一度4段パイプラインの実行図を見ながら、例を挙げて解説します。

a378_pipe_4stage.png

 上図の赤枠内等、EAステージと、先行命令のWBステージは同時に実行されます。
 EAステージ並びにWBステージの終了時にレジスタへの書き込みが発生するので、「レジスタへの書き込み」と言う資源が競合することになります。

 あるいは、先行命令のWBが書き込むレジスタを後続命令のEAステージが参照(読み出し)する場合はどうでしょうか?後続命令のEAが読み出す値は、先行命令のWBステージが書き込み(更新)した、新しい値でなくてはなりませんが、EAの読み出し時間は開始時であるのに対して、WBの書き込み時間は終了時なので、タイミングが合いません。
 こちらは、同一レジスタ(資源)へ対する、書き込みと読み出しが競合する例です。

 設計に当たって、この様な競合状態を対策して解決する必要が生じます。詳しく検討・考察して行きましょう。

レジスタ競合

 まず、レジスタファイル📂のアーキテクチャから考察してみましょう。

レジスタファイルのアーキテクチャ

 手元のARM教科書では、レジスタファイルを特殊なRAMで実装する試みが紹介されていました。
 ARM教科書では、CPUの演算やメモリアクセスなどに於いて、レジスタファイル内の汎用レジスタ2個を同時に読み出し、1個の汎用レジスタに書き込む事を想定しています。
 レジスタファイルをRAMで実装する場合、必要となる機能は以下の通りです。

 複数サイクルかけてアクセスするならともかく、通常は、上の機能が1サイクルで同時に実行される必要があります。つまり、特殊な3ポートRAMが必要だという事です。
 FPGAなどでも、同時に2組のポートから読み/書き可能なデュアル(2)ポートRAM等が用意されています。普通の(シングル(1)ポート)RAMは読み/書き可能なポートが1組しか無く、読み出しと書き込みを同時に実行する事はできません。そこからすると、デュアルポートRAMでさえ、かなり特殊なRAMであるという事です。

 にもかかわらず、ARMのレジスタファイルを実装するためには、3ポートRAMが必要だという事で、更に輪をかけて特殊なのです……。

 何しろ、ARMのレジスタファイルは、32ビット汎用レジスタが16本に加え、システムモードで置き換わるレジスタセットが数組あるため、1kビットを超えます(※)。集積密度の低かった昔の製造プロセスでは、特殊な3ポートRAMを使ってでも集積密度を上げたかったのでありましょう。
 しかし、最近の高集積密度な製造プロセスでは、特殊な3ポートRAMを用意する費用に比べると、フリップフロップF/Fを使い、RTLで実装してしまった方が有利なのです。

※Nihoniumのプログラマモデルは、32ビット汎用レジスタが8本と、汎用レジスタバンクが2組、制御レジスタが5本なので、総ビット数約600ビットと、ARMの半分程。

 前節の考察で述べた様に、今回Nihoniumへ適応する4段パイプラインでは、EXステージとWBステージが同時にレジスタファイルへ書き込みます。つまり、ARM教科書が想定した「1本の書き込みポート」では不足です。

 これらを踏まえて、以下の結論としました。

  レジスタファイルに関する考察の結論:

 RAMで実装するのは現実的ではありませんが、合成可能なRTLであれば、この程度は問題ではありません。

レジスタファイルの設計

 前節の考察を踏まえて、レジスタファイルのブロック図は以下の様になりました。

a37a_rgfblk.png

 基本的には、Nihonium用レジスタファイル(読み出し2本、書き込み1本だった)のRTLから流用します。書き込み用のCバスはEXステージからの書き込み用に割り当てて、新たにWBステージからの書き込み用としてDバスを追加しました(上図)。

 上図でbankブロックは、汎用レジスタバンクです。内部に8本の16ビット汎用レジスタr0〜r7が格納されています(下図)。

a37a_grbank.png

 汎用レジスタバンクにも、WBからの書き込み用Dバスを追加しました。汎用レジスタバンクの構成などはNihoniumと同等です(以下の通り)。

 全汎用レジスタおよび制御レジスタの書き込み口に、Dバスからの書き込み口が追加となりました。追加論理としては2 to 1セレクタ1段ですが、総ビット数は約600ビットと多いので、相当数の論理規模増大が予測されます。

書き込みレジスタの競合

 パイプライン方式で実装するに当たって、レジスタファイルを巡る競合状態について考察して行きます。

 レジスタファイルへの書き込みは、EXステージとWBステージの終了時で同時に発生して、書き込みタイミングが競合します。例えば、以下図の様な場合です。

  例)レジスタ書き込みタイミング競合
a37a_pipe_wbea1.png

 赤色縦線のタイミングに於いて、WBによる汎用レジスタr0への書き込みタイミングと、EXによるR1への書き込みタイミングが競合しています。書き込みレジスタ自体は非競合(異なるレジスタ)な場合です。
 割り切って、レジスタファイルで2個のレジスタに対する同時書き込みを許容した事により、WBとEXの同時書き込みは、そもそも競合しません。つまり、全く問題なしです。

 唯一競合する場合は、書き込むレジスタが完全に一致する、以下の様な場合です。

  例)書き込みレジスタ競合(同一レジスタ)
a37a_pipe_wbea2.png

 命令の実行順から考えて、実行後R0に残っているべき値は、後続のmov命令が書き込む値であるべきです。従って、EXとWBの書き込みレジスタが一致した場合は、WB書き込みを無視、EX書き込みを採用するのが正しいです。

 なお、この例は命令列としては有り得るけど、プログラムとしてはldw命令の実行意味が無い様に思えます。読み出したデータをすぐ次の命令で上書きして捨ててしまっているからです。
 ただ、全く無意味か?と言うと、そうとも言い切れません。
 R7が指し示す読み出しアドレスが、何らかのI/OユニットのI/Oレジスタだった場合で、「読み出す」と言う行為によって、何らかの状態が変化する場合等が相当します。例えばFIFOからのデータ読み出し(ただしデータは破棄する場合)等が該当します。
 読み出したデータ自体は不要だけど、読み出すという行為自体は必要と言う事です。

 いずれにせよ、EXとWBからの同一レジスタに対する同時書き込みは、EXを優先する事で解決です。

書き込みレジスタと読み出しレジスタの競合

 レジスタファイルへの同時書き込みを2系統とすることで、競合自体を回避した前節の例と異なり、WBステージに於ける書き込みレジスタと、EAステージに於ける読み出しレジスタの競合は、厄介です。詳しく見てみましょう。

  例)書き込みレジスタと読み出しレジスタ競合
a37a_pipe_wbea3.png

 回避方法は、以下図の通りです。

a37a_pipe_wbea4.png
※パイプライン実行図の凡例はこちら

 WBの終了時までEXがストールして、R0の更新を待ちます。EXがWBの終了を待ってストールした事により、R0の更新タイミングと参照タイミングが揃いました(赤色縦線)。この時、ストールしたEXを無視してIF/IDの処理を先に進める事はできないので、IF/IDもストールすることになります。

※Nihoniumでは上図の通り、EXがストールしてWBの終了を待つ事としたが(ハードウェア案件)、MIPSでは遅延ロード命令と称して、コンパイラに任せている(ソフトウェア案件)。

 実は、命令の実行順を下図の様に入れ替えると、ストール無しでR0の更新タイミングと参照タイミングが揃います。

a37a_pipe_wbea5.png

 上の例では、命令の実行順を入れ替えても、処理内容としては変わりありません。WBとEXのレジスタ競合を回避できて、IF/ID/EXのストールが解消されたことにより、1サイクル高速に実行できました。これは、命令スケジューリング最適化の一例であり、最適化コンパイラの範疇(守備範囲)です。

 たったこれだけ↑の事でパフォーマンスが上がりますので、是非とも頭に入れておいて下さい。


 レジスタファイルの設計が完了しましたので、次回はパイプラインの乱れ(ハザード)について検討したいと思います。


btn_arduino.png

btn_prev.png btn_next.png

home