Posts Tagged with "RISC-V"

既に発行済みのブログであっても適宜修正・追加することがあります。
We may make changes and additions to blogs already published.
posted by sakurai on October 28, 2022 #541

<PC>の必要性

従来例を再掲します。作成しようとするパイプラインロジックは大略、前の図540.1のとおりです。ただし図540.1では<PC>が明示されていません。

図%-%.1
図540.1 あるRISC-Vプロセッサのパイプライン図(再掲)

次の命令ストリームの起点であるPCは、基本的に

  1. 分岐が無ければ(有っても投機的に数命令は実行)、現PC+4
  2. PC相対分岐が有ればPC+オフセット
  3. 絶対分岐の場合はその値

を次のPCにセットします。この他にも割り込み、例外処理等で新PCを演算し、次のPCにセットします。これを<PC>を意識した図に書き直すと、図541.1のように判りやすい図となります。図540.1のように、PC演算器やレジスタが窮屈に<IF>の中に折れ曲がることは無く、ストレートフォワードに描かれています。<PC>の入力が<PC>であることも図541.1を見れば明らかです。

図%%.1
図541.1 <PC>の考え方に基づき書き直したパイプライン図

図541.1では分岐の場合は<EX>の結果レジスタがPCであることから元の<EX>と分岐先<PC>が重なり、その次のステージが分岐先<IF>となることが良くわかります。クロックベースが原則のパイプラインを記号で書けば、

   <PC><IF><ID><EX><WB>
          <PC><IF><ID><EX><WB>

となります。分岐先のPC計算を分岐元のEXで代行している様子が良く分かります。

一方、図540.1では<EX>の後にPCレジスタが入り、その次に<IF>ステージとなるので、記号で書くと、

   <IF><ID><EX><WB>
          <IF><ID><EX><WB>

となり、<IF>が湧き出す意味が不明です。<IF>の入力レジスタがPCで、分岐命令の<EX>でそれを生成しているのだからという説明になると思いますが、であればPCを明示したほうが分かりやすいのは言うまでもありません。

逆にPCの生成も含めたパイプラインを理解してしまえば頭の中でできるため、取り立てて<PC>の必要性は感じなくなるので、不思議とも思わないようになりますが、教育的とは言えません。そもそも最も重要なPCの更新ステージはどこなのかが図示されていません。これでは分岐命令の高速化、例えばBTB等による<PC>の物量増加も、どのステージの話なのか理解しづらくなります。

<PC>の制御法

<PC>の必要性が理解できたところで、過去記事でパイプライン制御論理を示しました。<PC>の上流が<PC>だからといって上流に出力するwait信号を自分自身に入れると永久にストールするので、自分自身に入れてはいけません。

また、図示されていませんが、PCレジスタ自体も同じパイプライン制御論理によりパイプラインを流します。その理由は、例外の際には分岐が発生しますが、戻りアドレスをスタックに格納します。その戻りアドレスを上記PCパイプラインから適宜取得するためです。


左矢前のブログ 次のブログ右矢

posted by sakurai on October 27, 2022 #540

PCステージ

PCステージ(<PC>)を設計します。と言うとなじみがありませんね。一般には存在しないステージなので。以降ではステージ記号を次のように定義します。例:PCステージ:=<PC>

一般にはパイプライン図は<IF>から始まっています。例えば図540.1のように。PCレジスタは<IF>の入力レジスタとして描かれています。良く見ると、本来の<PC>の演算器や結果レジスタは折り曲げられて、窮屈な恰好をしています。

図%%.1
図540.1 あるRISC-Vプロセッサのパイプライン図(引用元)

図540.2の黄色のステージ記号及び点線は弊社で追加しました。FDレジスタとは<IF>と<ID>の間のステージなので、<IF>の結果レジスタです。つまりFDレジスタの左側は全て<IF>です。この図でもPCレジスタは窮屈に折り曲げられています。

図%%.1
図540.2 あるRISC-Vプロセッサのパイプライン図(引用元)

この図のように<IF>の中にPCレジスタが書かれています。これが変であることに気づかれたでしょうか。パイプラインプロセッサは、パイプラインレジスタにより、組み合わせ回路の結果を次々に受けていくバケツリレー式であり、本来ステージの内部は組み合わせ回路で構成され、レジスタは無いはずです。

それでは<IF>の入力である命令アドレスはどこのステージで生成されるのでしょうか?それが<PC>です。つまり一般の図ではPCは<IF>の入力レジスタのように描かれていますが、実は他のステージと同様、<PC>の結果レジスタです。こう考えるとパイプラインの各ステージが統一的に理解できます。

例えば分岐命令では分岐先アドレス計算をする必要があり、それは必ず<ID>の後になります。図540.2にもPC calculationとありますが、それがパイプライン中の2個目の<PC>です。そして分岐条件が確定した後に<IF>が実行されるので、<PC>は明示したほうが判りやすいです。

一般的に<PC>が無視される理由としては、有名な教科書がIF, ID, EX, MEM, WBの5段で書かれている、からなのかもしれませんが、本来は

<PC><IF><ID><EX><MA><WB>

の6段パイプです。このことは過去記事でも指摘しています。

次の質問です。<PC>の前のステージは何でしょうか?答えは<PC>です。分岐しない限り<PC>は次の<PC>を生成し、1サイクル毎に次々にストリームを生み出します。

一方、例えば分岐命令等のようにパイプラインの途中に<PC>が出現することがあり、複数現れる場合でも命令ストリームの開始PCの計算ですから、いきなり別の命令ストリームの<IF>が現れるよりは判りやすいと思います。無から有は湧いてきません。

割り込みや例外を考える時には一層重要です。割り込みレベルやマスクや例外の種類等の情報を総合して、まずPCがどうなるかを決定します。PCさえ確定すれば、後は<IF>以降のパイプラインを普通に流せば良いだけです。つまり常に<PC>が命令ストリームの起点となります。

本稿で述べたことはエンジニアとしては意識しなくても、設計できるし、見方を変えて設計が変わるわけではないので、哲学に属する話かもしれません。

しかし、設計思想として大事な話なので強調しています。この思想のメリットもあり、例えば<PC>においてPCアドレスを命令アドレスとして命令メモリに流し、<PC>と<IF>の間のクロックで命令アドレスをラッチし、<IF>として命令メモリからデータを流し、プロセッサは<IF>と<ID>の間のクロックで命令データをラッチする、というフローが一般的ですが、<PC>を<IF>の中に混ぜると<IF>が複雑になります。一方この考え方であれば、ラッチベースの動作がすっきりします。


左矢前のブログ 次のブログ右矢

posted by sakurai on October 25, 2022 #536

デコーダのソースの一部を示します。

genRules(
   switch(in_instr,
      when(pat(n(7'b0000000), v, v, n(3'b000), v, n(7'b0110011)), fadd),
      when(pat(n(7'b0100000), v, v, n(3'b000), v, n(7'b0110011)), fsub),
      when(pat(               v, v, n(3'b000), v, n(7'b0010011)), faddi),
      when(pat(n(7'b0000000), v, v, n(3'b111), v, n(7'b0110011)), fand),
      when(pat(n(7'b0000000), v, v, n(3'b110), v, n(7'b0110011)), ffor),
      when(pat(n(7'b0000000), v, v, n(3'b100), v, n(7'b0110011)), fxor),
      when(pat(               v, v, n(3'b111), v, n(7'b0010011)), fandi),
      when(pat(               v, v, n(3'b110), v, n(7'b0010011)), fori),
      when(pat(               v, v, n(3'b100), v, n(7'b0010011)), fxori),
      when(pat(               v, v, n(3'b010), v, n(7'b0000011)), flw),
      when(pat(            v, v, v, n(3'b010), v, n(7'b0100011)), fsw),
      when(pat(n(7'b0000000), v, v, n(3'b001), v, n(7'b0110011)), fsll),
      when(pat(n(7'b0000000), v, v, n(3'b101), v, n(7'b0110011)), fsrl),
      when(pat(n(7'b0100000), v, v, n(3'b101), v, n(7'b0110011)), fsra),
      when(pat(n(7'b0000000), v, v, n(3'b001), v, n(7'b0010011)), fslli),
      when(pat(n(7'b0000000), v, v, n(3'b101), v, n(7'b0010011)), fsrli),
      when(pat(n(7'b0100000), v, v, n(3'b101), v, n(7'b0010011)), fsrai),
      when(pat(n(7'b0000000), v, v, n(3'b010), v, n(7'b0110011)), fslt),
      when(pat(n(7'b0000000), v, v, n(3'b011), v, n(7'b0110011)), fsltu),
      when(pat(               v, v, n(3'b010), v, n(7'b0010011)), fslti),
      when(pat(               v, v, n(3'b011), v, n(7'b0010011)), fsltiu),
      when(pat(v, v, v, v, n(3'b000), v, v, n(7'b1100011)), fbeq),
      when(pat(v, v, v, v, n(3'b001), v, v, n(7'b1100011)), fbne),
      when(pat(v, v, v, v, n(3'b100), v, v, n(7'b1100011)), fblt),
      when(pat(v, v, v, v, n(3'b101), v, v, n(7'b1100011)), fbge),
      when(pat(v, v, v, v, n(3'b110), v, v, n(7'b1100011)), fbltu),
      when(pat(v, v, v, v, n(3'b111), v, v, n(7'b1100011)), fbgeu),
      when(pat(v, v, v, v, v, n(7'b1101111)), fjal),
      when(pat(               v, v, n(3'b000), v, n(7'b1100111)), fjalr),
      when(pat(               v, v, n(7'b0110111)), flui),
      when(pat(               v, v, n(7'b0010111)), fauipc),
      when(pat(               v, v, n(3'b001), v, n(7'b1110011)), fcsrrw),
      when(pat(               v, v, n(3'b101), v, n(7'b1110011)), fcsrrwi),
      when(pat(               v, v, n(3'b010), v, n(7'b1110011)), fcsrrs),
      when(pat(               v, v, n(3'b110), v, n(7'b1110011)), fcsrrsi),
      when(pat(               v, v, n(3'b011), v, n(7'b1110011)), fcsrrc),
      when(pat(               v, v, n(3'b111), v, n(7'b1110011)), fcsrrci),
      when(pat(n(25'b0), n(7'b1110011)), fecall)
   ) // switch
);

これは1ステップ目のデコーダステップであり、ここでビットパターン、例えばaddi命令とのマッチが取れれば、2ステップ目として個別の関数、例えばfaddiが呼び出されます。一例であるfaddiを示せば、

function Action faddi(Bit#(12) imm, Bit#(5) rs1, Bit#(5) rd) = 
   action
      Int#(32) immSext = signExtend(unpack(imm));
      if (immSext == 0)
         $display("time %4t -   mv\t%s,%s", $time, regname(rd), regname(rs1));
      else if (rs1 == 0)
         $display("time %4t -   li\t%s,%0d", $time, regname(rd), immSext);
      else
         $display("time %4t -   addi\t%s,%s,%0d", $time, regname(rd), regname(rs1), immSext);
   endaction;

RISC-Vにおいてaddi命令はイミディエイトがゼロの場合はmv命令として使用され、逆にソースレジスタにゼロレジスタを指定すれば、イミディエイトロード(li)命令として働きます。これらはプロセッサの設計的には不要な処理ですが、逆アセンブラのシンタックスシュガーとして実装しました。

2ステップ目の処理として、各種関数を命令数だけ並べる必要があります。


左矢前のブログ 次のブログ右矢

posted by sakurai on October 24, 2022 #535

BSV実行結果(ハードウェアからの出力)と逆アセンブルリストをサイドバイサイドに並べた図を535.1に示します。分岐関係を除いて一致していることが分かります。BSV実行出力にアドレスやデータを表示することは簡単ですが、逆アセンブラを作成しているわけではないので、それらの表示機能は実装していません。

図%%.1
図535.1 実行結果と逆アセンブルリスト

この後も「はじめてのCPU自作」にあるようにECALLやCSR操作命令等を実装し、ひととおりriscv-testが通るデコーダまで作成が完了しました。ところで論理合成ツールには論理圧縮機能が含まれるため、出力されたデコーダ論理を合成前に圧縮する必要はありません。


左矢前のブログ 次のブログ右矢

posted by sakurai on October 21, 2022 #534

コンパイルしたオブジェクトファイルの逆アセンブルリストは以下のとおりです。

図%%.1
図534.1 逆アセンブルリスト

これを「ハードウェアインタプリター」にかけたら以下のような実行結果となりました。

図%%.2
図534.2 インタプリター実行結果

addi命令のイミディエイトをゼロにすることでmv命令としたり、逆にレジスタをゼロレジスタとすることでli命令としたり、「インタプリター」の出力を細工し、逆アセンブルリストと合わせています。ただし、PCを実装していないので、PC相対命令のラベルは一致していません。このようにPC相対を除き、逆アセンブル結果とほぼ合わせることができました。


左矢前のブログ 次のブログ右矢

posted by sakurai on October 20, 2022 #533

「ハードウェアインタプリター」に食わせる機械語列が必要です。そこで、この記事を参考に、Fibonacciプログラムをコンパイルし機械語化しました。試みにFibonacciが通るための「インタプリター」を書いていきます。前述のとおりこの「インタプリター」は実行ステージが逆アセンブル相当の表示をするだけのものです。

入力するFibonacciのソースは以下のように短いプログラムです。

fibo.c:

int fib(int n) {
  if(n <= 1) return 1;
  return fib(n-1) + fib(n-2);
}
int main() {
  fib(10);
  for(;;) {}
  return 0;
}

これをクロスコンパイルし、BSVの入力とします。シーケンサの自動生成を利用して1サイクル毎に、命令デコーダに命令を供給します。

Stmt main = seq
    instr <= 32'h074000ef;
    instr <= 32'hfe010113;
    instr <= 32'h00112e23;
    instr <= 32'h00812c23;
    instr <= 32'h00912a23;
    instr <= 32'h02010413;
    instr <= 32'hfea42623;
    instr <= 32'hfec42703;
    instr <= 32'h00100793;
    instr <= 32'h00e7c663;
    instr <= 32'h00100793;
    instr <= 32'h0300006f;
    instr <= 32'hfec42783;
    instr <= 32'hfff78793;
    instr <= 32'h00078513;
    instr <= 32'hfc9ff0ef;
    instr <= 32'h00050493;
    instr <= 32'hfec42783;
    instr <= 32'hffe78793;
    instr <= 32'h00078513;
    instr <= 32'hfb5ff0ef;
    instr <= 32'h00050793;
    instr <= 32'h00f487b3;
    instr <= 32'h00078513;
    instr <= 32'h01c12083;
    instr <= 32'h01812403;
    instr <= 32'h01412483;
    instr <= 32'h02010113;
    instr <= 32'h00008067;
    instr <= 32'hff010113;
    instr <= 32'h00112623;
    instr <= 32'h00812423;
    instr <= 32'h01010413;
    instr <= 32'h00a00513;
    instr <= 32'hf7dff0ef;
    instr <= 32'h0000006f;
endseq;

今回はまだデコーダのテストだけなので、PCは実装していません。


左矢前のブログ 次のブログ右矢

posted by sakurai on October 18, 2022 #532

図532.1にBSV版のBitpatのREADMEに掲載されていた使用例を示します。

図%%.1
図532.1 BSVのBitpat関数

挙げられた例はちょうどRISC-Vの命令パターンに一致しており、add命令とaddi命令のデコード部分を示したものです。この要領で、次々に他の命令を実装していくことができます。

本ライブラリの動作は以下の2ステップとなっています。

  1. whenの中のパターンマッチでは可変部をvで表し、固定部をnとビットパターンで記述します。vの幅を指定しないで良いのは使いやすそうです。whenの最後に識別した機能を解釈するための関数名を記述します。
  2. マッチした後に呼ばれる関数では、可変部のみを変数で受け(固定部は捨てる)、処理を実行します。結局vの幅は意識しなければなりません。

このように最初に固定部、次に可変部という考え方に慣れる必要があります。最初は使いにくいと感じましたが、慣れれば気にならないのかもしれません。

プロセッサ設計と言ってもパイプラインでなければ、見方を変えれば、RISC-V機械語のインタプリターをHDLで作成するだけなので、それほど難しいことではありません。ひとつずつデコードし、対応する処理を実装していくだけです。この「ハードウェアインタプリター」の1段目はデコードステップで、2段目は実行ステップになります。


左矢前のブログ 次のブログ右矢

RISC-Vプロセッサの設計

posted by sakurai on October 17, 2022 #531

「はじめてのCPU自作」という本を購入したので、これを参考にRISC-Vプロセッサを設計します。ただし、この本ではChiselベースとなっていますが、本稿ではBSVベースとします。またパイプラインプロセッサの経験があるため、最初からパイプラインプロセッサを設計します。

さて、この本を読んでいたらChisel(だかScalaだか)にはBitpatという便利な機能があるようです。

図%%.1
図531.1 ChiselのBitpat関数

命令デコーダを書くのに便利そうなので、BSVにもないのか調べたら、GithubにBitpatという似たようなものがありました。これはAlexandre Joannouさんが作成されたものであり、Readmeには以下のように書いています。

BitPat BitPat is a bit-string pattern matching library for Bluespec, inspired by Morten Rhiger's "Type-Safe Pattern Combinators".

BSVにおいてのパターンマッチライブラリとのことです。便利そうなのでRISC-Vの命令デコーダに採用することにします。


左矢前のブログ 次のブログ右矢

RISC-Vの調査 (9)

posted by sakurai on June 22, 2020 #280

ソースの解読

ここからはソースを読んで行きます。パイプラインプロセッサは、その名のとおり各ステージがパイプラインとして動作するもので、つまりステージ毎にステージを構成するレジスタでできています。逆に、レジスタ以外は全て組み合わせ回路です。これに気づくとソースの理解が速そうです。

難しいのはパイプラインレジスタの更新ロジックで、パイプラインが動作したりストールしたりするのはこのレジスタ更新論理ですが、一方、ステージの実質のロジックは単なる組み合わせ回路です。最も分かりやすいのが演算ステージStage1で、ALU等はファンクション(組み合わせ回路)で記述されます。

それらのファンクションはパイプラインのステージからコールされ、パイプラインにはパイプライ制御のロジックしかなくなります。従って、Fluteの複雑そうな各パイプラインステージは非常に簡単な記述になっています。表280.1に各ステージのbsvで記述された行数を示します。

表280.1 パイプラインステージとその行数
ファイル名 行数
CPU_StageF.bsv 170
CPU_StageD.bsv 153
CPU_Stage1.bsv 336
CPU_Stage2.bsv 627
CPU_Stage3.bsv 245


左矢前のブログ 次のブログ右矢

RISC-Vの調査 (8)

posted by sakurai on June 19, 2020 #279

ループにおける効率調査

前稿のプログラムは命令試験プログラムだったので、キャッシュの効果が出ていませんでした。そのため、ここではループにおける効率の向上を調査します。合わせてgccの効率も調べます。

int main() {
  for (int j = 0; j < 100; j++) {
    for (int i = 0; i <= 9; i++) {
      *UART0_ADDR = '0'+i;
    }
    *UART0_ADDR = '\n';
  }

このようなループの関数を作成し、gccにより-O, -O2, -O3でコンパイルします。まず+v1で実行命令数を確認すれば、それぞれ、5617、5517、2422実行命令数となりました。

次に+v2でパイプラインの状況を確認します。

表279.1 ステージ毎のパイプラインの状態(-O)
状態 StageF StageD Stage1 Stage2 Stage3
BUSY 91 0 1,150 2,251 0
EMPTY 106 198 178 1,410 3,664
PIPE処理 9,187 9,186 8,056 5,723 5,720
合計 9,384

表279.2 ステージ毎のパイプラインの状態(-O2)
状態 StageF StageD Stage1 Stage2 Stage3
BUSY 88 0 23 2,259 0
EMPTY 106 172 170 284 2,544
PIPE処理 7,972 7,994 7,973 5,623 5,622
合計 8,166

表279.3 ステージ毎のパイプラインの状態(-O3)
状態 StageF StageD Stage1 Stage2 Stage3
BUSY 152 0 1,023 2,264 0
EMPTY 4 131 131 1,138 3,403
PIPE処理 5,672 5,697 4,674 2,426 2,425
合計 5,828

これらの表の合計欄がマシンサイクルを意味しています。従って、最適化度合いに対するCPIは、-O, -O2, -O3のそれぞれで1.67、1.48、2.41となりました。

表279.3の-O3においてはインナーループを展開しているため、命令キャッシュミスが若干増加しており、デコードステージ以下のパイプラインバブルが発生していますが、命令数をぐっと少なくしてマシンサイクルを縮めています。いずれも表264.1と比較すると、ループであるため命令キャッシュミスによるパイプラインストールがかなり少なくなっており、効率が向上しています。


左矢前のブログ 次のブログ右矢


ページ: