FPGA開発日記

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

SonicBOOMに関する調査 (7. Load-useに関する調査)

SonicBOOMのロードユースについて調査したい。キャッシュヒットした場合、ロード命令からその次の依存命令まで何サイクル掛かるのか。

以下のようなテストプログラムを作成した。ロードした命令を次にadd命令で使用する。ヒットしたときとそうでないときでレイテンシの差分を見たい。

 int64_t data [16 * 4] __attribute__ ((aligned (64))) = {
   0x100, 0x101, 0x102, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
   0x110, 0x111, 0x112, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
   0x120, 0x121, 0x122, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
   0x130, 0x131, 0x132, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f
 };

 volatile int init_wait = 0;

 int main (int hartid)
 {
   int64_t load;

   __asm__ __volatile__ ("ld  %0, %1":"=r"(load): "m"(data[0]));
   __asm__ __volatile__ ("add %0, %1, 0x10":"=r"(load): "r"(load));
   __asm__ __volatile__ ("add %0, %1, 0x20":"=r"(load): "r"(load));

   data [16*4-1] = load;

   __asm__ __volatile__ ("fence");

   __asm__ __volatile__ ("ld  %0, %1":"=r"(load): "m"(data[2]));
   __asm__ __volatile__ ("add %0, %1, 0x20":"=r"(load): "r"(load));
   __asm__ __volatile__ ("add %0, %1, 0x30":"=r"(load): "r"(load));

   data [16*4-2] = load;

 }

結果はこのようになった。最初のロード命令から次のADD命令までが6サイクル、次のロード命令から次のADD命令までが5サイクル。2番目のロード命令はキャッシュヒットしているのでレイテンシはもう少し短くなると思ったのだが、なんで5サイクルもかかっているんだ?

                 832 3 0x0000000080001784 auipc   a4, 0x0 x14 0x0000000080001784
                 838 3 0x0000000080001788 addi    a4, a4, 508 x14 0x0000000080001980
                 // 1発目のロード命令(これはキャッシュミス)
                 858 3 0x000000008000178c c.ld    a5, 0(a4) x15 0x0000000000000100
                 // Load→ADDまで6サイクル
                 864 3 0x000000008000178e c.addi  a5, 16 x15 0x0000000000000110
                 865 3 0x0000000080001790 addi    a5, a5, 32 x15 0x0000000000000130
                 865 3 0x0000000080001794 sd      a5, 504(a4)
                 901 3 0x0000000080001798 fence
                 // 2発目のロード命令(これはキャッシュヒット)
                 914 3 0x000000008000179c c.ld    a5, 16(a4) x15 0x0000000000000102
                 // Load→ADDまで5サイクル
                 919 3 0x000000008000179e addi    a5, a5, 32 x15 0x0000000000000122
                 920 3 0x00000000800017a2 addi    a5, a5, 48 x15 0x0000000000000152
                 920 3 0x00000000800017a6 c.li    a0, 0 x10 0x0000000000000000
                 921 3 0x00000000800017a8 sd      a5, 496(a4)
                 921 3 0x00000000800017ac ret
                 922 3 0x0000000080001648 addi    s0, gp, -1984 x 8 0x0000000080001bc0
                 937 3 0x000000008000164c c.ld    a3, 0(s0) x13 0x0000000000000000
                 938 3 0x000000008000164e addi    s2, sp, 63 x18 0x0000000080021bcf
                 938 3 0x0000000080001652 andi    s2, s2, -64 x18 0x0000000080021bc0
                 939 3 0x0000000080001656 c.mv    s3, a0 x19 0x0000000000000000
                 943 3 0x0000000080001658 c.bnez  a3, pc + 44
f:id:msyksphinz:20210614235757p:plain

一応、ld_missという信号線があって、これがアサートされない場合(L1Dにヒットした場合)1サイクル速くなるらしいけど、それでも5サイクルは遅すぎる。どうしてそうなるんだろう?詳しく調査する。

テストパタンは以下のようになっている。

0000000080001784 <main>:
    80001784:   00000717                auipc   a4,0x0
    80001788:   1fc70713                addi    a4,a4,508 # 80001980 <data>
    8000178c:   631c                    ld      a5,0(a4)
    8000178e:   07c1                    addi    a5,a5,16
    80001790:   02078793                addi    a5,a5,32
    80001794:   1ef73c23                sd      a5,504(a4)
    80001798:   0ff0000f                fence
    8000179c:   6b1c                    ld      a5,16(a4)
    8000179e:   02078793                addi    a5,a5,32
    800017a2:   03078793                addi    a5,a5,48
    800017a6:   4501                    li      a0,0
    800017a8:   1ef73823                sd      a5,496(a4)
    800017ac:   8082                    ret

RISC-Vベクトル拡張仕様書 v1.0 を読み直す

RISC-Vベクトル拡張が V1.0のためのRC1になったので、改めて読み直してみることにした。 数年前にベクトル拡張の仕様書0.8あたりを日本語に翻訳したのだが、同じように日本語に変えながら理解していく。

この翻訳の成果は、以下に格納している。

github.com

v1.0-rc1からの変更点

現在まで変更なし。

1. イントロダクション

このドキュメントは、RISC-Vベクトル拡張のバージョン1.0の第2リリース候補のドラフトであり、 パブリックレビューのためのものです。

これはパブリックレビュー用の1.0の凍結版ではありません。


最終的に承認され、リリース候補のタグが外されたバージョン1.0は、 RISC-Vインターナショナルの批准プロセスの一環として、パブリックレビューのために送り出されることを意図しています。 また、バージョン1.0は、上流のソフトウェアプロジェクトを含め、ツールチェーン、機能シミュレータ、 初期実装の開発を開始するのに十分な安定性があると考えられており、 批准中に重大な問題が発見された場合を除き、 大きな機能変更はないと予想されています。批准されると、仕様書のバージョンは2.0になります。


本仕様書ドラフトには、現在定義されているベクトル命令の全セットが含まれています。 Standard Vector Extensions セクションには、標準的なベクトル拡張機能と、 それぞれの拡張機能でサポートされる命令および要素幅が記載されています。

  1. 実装により定義される定数パラメータ ベクトル拡張をサポートする各hartには、2つのパラメータが定義されています。

任意の演算で生成または消費できるベクター要素の最大サイズ(ビット) ELEN ≥ 8 で、これは2の累乗でなければなりません。

1つのベクトルレジスターのビット数 VLEN 、これは2の累乗でなければならず、2の累乗であり、216以上であってはなりません。

標準的なベクトル拡張(Standard Vector Extensions セクション)とアーキテクチャプロファイルは、 ELEN と VLEN にさらなる制約を設定することができます。


VLENの上限により、ソフトウェアはインデックスが16ビットに収まることを前提に組み立てることができます (LMUL=8およびSEW=8でVLEN=65,536の場合、最大のVLMAXは65,536になります)。 将来的にクトルレジスタあたり64Kib以上に拡張する場合は、 新しいコンフィグレーション命令が必要になりますが、 従来のコンフィグレーション命令を使用しているソフトウェアでは、 ベクトルの長さが大きくなることはありません。


ISAは、特定の制約の下で、VLENパラメータの値が異なるhart上でバイナリコードが実行されるポータブルな記述をサポートしていますが、 両方のhartが必要な要素型をサポートしていることが条件となります。


実装パラメータの違いを露呈するようなコードを書くことも可能です。



一般的に、ベクトルのステートがアクティブであるスレッドコンテキストは、 VLENまたはELENパラメータに違いのあるhart間で実行中にコンテキストを移動するとはできません。


3. ベクトル拡張のプログラミングモデル

ベクトル拡張はRISC-VのベーススカラISAに対して、 32本のベクトルレジスタと7つの非特権CSR(vstart, vxsat, vxrm, vcsr, vl, vlenb)が追加されます。

3.1. ベクトルレジスタ

ベクトル拡張により、RISC-VベーススカラISAに対して 32本のアーキテクチャベクトルレジスタv0-v31が追加される。

各ベクトルレジスタのサイズは固定長でVLENビットである。


Zfinx ("F in X") は現在議論が行われている、浮動小数点命令を 整数レジスタファイル上で実行する新しいISAオプションです。 Vector 1.0拡張はZfinxに対して互換性があります。


3.2. mstatus 内のベクトルコンテキストステータス

ベクトルコンテキストステータスフィールド VS は、 mstatus[10:9] に追加され、 sstatus[10:9] でシャドウイングされます。 これは、浮動小数点コンテキストステータスフィールドである FS と同様に定義されます。

VS フィールドがOFFに設定されている場合は、 ベクトル命令を実行しようとしたり、ベクトルCSRにアクセスしようとすると、 不正命令例外が発生します。

VS フィールドがInitialまたはCleanに設定されている場合、 ベクトルCSRを含むベクトルの状態を変更する命令を実行すると、VS がDirtyに変更されます。 また、実装では、ベクトルの状態が変化していなくても、 いつでもVS フィールドをInitialまたはCleanからDirtyに変更することができます。


VS フィールドの正確な設定は最適化に繋がります。 ソフトウェアは通常、コンテキストスワップのオーバーヘッドを減らすためにVSを使用します。


実装では、書き込み可能な misa.v フィールドを持つことができます。 浮動小数点演算ユニットの処理方法と同様に、 misa.v がクリアされていても mstatus.vs フィールドが存在する場合があります。


misa.v がクリアされているときに mstatus.vs フィールドへのアクセスを許可すると、 ベクトル操作のエミュレーションが可能になり、書き込み可能な misa.v を持つシステムでの mstatus.vs の処理が簡単になります。


VerilogのメモリをVerilatorではどのように処理するのか

Verilogでは以下のように、いくつかの書き方でメモリを作ることができる。memory1は通常のは配列の構造、memory2連想配列となっている。SystemVerilogの仕様的に通常の配列はそのまま利領域がメモリに確保され、連想配列は動的に配列の要素が確保される仕組みになっている。

 logic [31: 0]          memory1[1024];
 logic [31: 0]          memory2[logic[9: 0]];

これらのメモリにアクセスするとき、VerilatorはどのようなC++ファイルを生成するのだろうか。実際に以下のような回路を記述して調査してみることにした。

 module memory
   (
    input logic         i_clk,
    input logic         i_reset_n,

    input logic         i_wr,
    input logic [9: 0]  i_addr,
    input logic [31: 0] i_data,
    output logic [31: 0] o_out1,
    output logic [31: 0] o_out2
    );

 logic [31: 0]          memory1[1024];
 logic [31: 0]          memory2[logic[9: 0]];

 always_ff @ (posedge i_clk, negedge i_reset_n) begin
   if (i_wr) begin
     memory1[i_addr] <= i_data;
     memory2[i_addr] <= i_data;
   end
   o_out1 <= memory1[i_addr];
   /* verilator lint_off WIDTH */
   o_out2 <= memory2.exists(i_addr) ? memory2[i_addr] : 'h0;
 end

 endmodule // memory

これをコンパイルしてC++ファイルを生成してみる。

$ verilator --cc --trace-fst --trace-params --trace-structs --trace-underscore memory.sv

obj_dir/Vmemory.hを確認してみる。

 VL_MODULE(Vmemory) {
   public:

/* ... 中略 ...*/
     // LOCAL SIGNALS
     // Internals; generally not touched by application code
     IData/*31:0*/ memory__DOT__memory1[1024];
     VlAssocArray<SData/*9:0*/, IData/*31:0*/> memory__DOT__memory2;

通常のメモリはC++の配列として表現されていた。一方で連想配列VlAssocArrayという型を使っているようだった。これはどういうものだろう?

https://github.com/verilator/verilator/blob/0f7ec6c9ba52160573df8a7ee90bcc38c837eee7/include/verilated_heavy.h

verilated_heavy.hに以下のように定義されていた。

//===================================================================
// Verilog associative array container
// There are no multithreaded locks on this; the base variable must
// be protected by other means
//
template <class T_Key, class T_Value> class VlAssocArray final {
private:
    // TYPES
    using Map = std::map<T_Key, T_Value>;

public:
    using const_iterator = typename Map::const_iterator;

private:
    // MEMBERS
    Map m_map;  // State of the assoc array
    T_Value m_defaultValue;  // Default value

public:
    // CONSTRUCTORS
    // m_defaultValue isn't defaulted. Caller's constructor must do it.
    VlAssocArray() = default;
    ~VlAssocArray() = default;
    VlAssocArray(const VlAssocArray&) = default;
    VlAssocArray(VlAssocArray&&) = default;
    VlAssocArray& operator=(const VlAssocArray&) = default;
    VlAssocArray& operator=(VlAssocArray&&) = default;

なるほど、テンプレートクラスなのか。そしてexists()clear(), next()などのメソッドが用意されている。これに置き換わるということか。分かってきた。

ということは、極端にメモリの確保量が大きくなければ、連想配列と通常の配列の使い分けとして、通常配列を使っておけばいいということが分かる。当然アクセス速度は通常配列の方が高そうなので、巨大でSparseなものでなければ、通常の配列を使っておけば良さそうだ。

SonicBOOMに関する調査 (6. BOOMの自己書き換えコードについての確認)

自作CPUで仮想アドレスのコードを流していると、TLBエラー発生時にユーザコードをコピーしてユーザ領域に移すコードが存在している。このコピー作業を終えた後にユーザモードに戻ってプログラムの実行を再開する。

この時に、コピーしたプログラムは当然L1Dキャッシュ上に存在しているのだが、これをフェッチで引っ張ってくる必要がある。

これを実現しているのが、TileLinkのスヌープインタフェースで、TileLinkのBチャネルを使ってL1Dからデータを吸い出している。これをBOOMで確認してみよう。

$ ./simulator-chipyard-MediumBoomConfig-debug --vcd=rv64ui-v-sb.medium.vcd \
    --verbose /mnt/c/usr/work/msrh/tests/riscv-tests/isa/rv64ui-v-sb 2>&1 | spike-dasm | tee rv64ui-v-sb.medium.log

このように、SRETでユーザモードに戻った後に0x80052b20にアクセスするのだが、これはマシンモードでメモリにストアしている。

      2771:S:Sv39:ffffffffffe00140:P000080000140:10200073:sret                            :mstatus=>8000000a00006000 mstatus<=8000000a00006020 sepc=>0000000000002b20 pc<=0000000000002b20
      2772:U:Sv39:0000000000002b20:P000080052b20:00001097:auipc      x01,0x00001          :x01<=0000000000003b20
      2773:U:Sv39:0000000000002b24:P000080052b24:4e008093:addi       x01,x01,0x4e0        :x01=>0000000000003b20 x01<=0000000000004000
      2774:U:Sv39:0000000000002b28:P000080052b28:faa00113:addi       x02,x00,0xfaa        :x02<=ffffffffffffffaa

この0x80052b20は、前のストア命令で設定している。

      2145:S:Sv39:ffffffffffe023e0:P0000800023e0:0066bc23:sd         x06,0x018(x13)       :x13=>0000000000002b00 x06=>00008067f18ff0ef (0000000080052b18)<=00008067f18ff0ef
      2153:S:Sv39:ffffffffffe023d4:P0000800023d4:01e6b023:sd         x30,0x000(x13)       :x13=>0000000000002b20 x30=>4e00809300001097 (0000000080052b20)<=4e00809300001097
      2154:S:Sv39:ffffffffffe023d8:P0000800023d8:01d6b423:sd         x29,0x008(x13)       :x13=>0000000000002b20 x29=>00208023faa00113 (0000000080052b28)<=00208023faa00113
      2155:S:Sv39:ffffffffffe023dc:P0000800023dc:01c6b823:sd         x28,0x010(x13)       :x13=>0000000000002b20 x28=>faa0039300008703 (0000000080052b30)<=faa0039300008703
      2156:S:Sv39:ffffffffffe023e0:P0000800023e0:0066bc23:sd         x06,0x018(x13)       :x13=>0000000000002b20 x06=>3c771c6300200193 (0000000080052b38)<=3c771c6300200193

これをBOOMのシミュレーション波形で確認すると、ちょうどSRETを終えた後にTileLink-Bチャネルを使ってスヌープが行われているようだ。

f:id:msyksphinz:20210612005251p:plain

RISC-V "V" Vector Extension の仕様が 1.0 Release Candidate-1となった

RISC-V Vector Extensionの仕様はv0.9からまさかのv0.10となり、いつになれば正式版になるのかと思っていたが、やっと1.0のRC1が公開されたようだ。といってもTagが打たれただけで、今度はRISC-V Internationalによる承認フローに入るものと思われる。

v0.10からの仕様変更部分について確認する。

github.com

  • マスクと vl の依存関係は、RVWMO の目的のための制御依存関係であることを明確にした。
    • これは良く分からん。RVWMOの部分に何となく説明が入っているが、備考の所を確認すると、要するにマスクに合わせて細かくロードを制御する必要がないということ?
  • Fault-only-firstのセグメントロード命令は、トリミングが発生した時点を超えてデスティネーションレジスタを更新することができることを指定。
    • Fault-only-firstでセグメントロードを行う場合は、Index=0で複数の要素を更新するが、VLによるトリミングを超えてレジスタを更新することができるということ?
  • whole registerロードのセクションで、オペコードにエンコードされたEEWにかかわらず、EEWは常に8であることを暗示する矛盾した文章を削除しました。
    • つまり8ビットのアドレスアライン制約を適用せず、16ビットの制約などに変更しても良いということ?
  • 標準ベクター拡張の名称と内容を記載したセクションを追加し,批准を求める。
    • 18章は"Standard Vector Extension"となり、Vector命令の拡張として再定義される。
  • 標準ベクトル拡張からベクトル AMO を削除しました。Zvamo拡張として後に追加されますが、以前の提案とは異なるエンコーディングが必要になります。
    • まじか!Atomicは標準から削除され再エンコードされる。
  • インデックス付きベクタメモリ命令のアドレスオフセットにEEWがサポートされていない場合、実装が不正命令例外を発生させることができることを明確にしました。
  • VLENの上限を64Kibとしました。
  • ベクトル長を最大にした場合に生成されるであろう値で、Mask-agnostic tailを書けるようにしました。
    • うーん、これが良く分からない?Mask-Agnosticの場合の挙動が変わるということ?
  • Masked ロード/ストア命令は、従来のEEW=1ではなく、EEW=8として動作することを明確にしました。
    • これは新たに導入されたvlm.vvsm.vのことだな。
  • アセンブラニーモニックである非順序浮動小数点リダクションのvfredsumとvfwredsumをそれぞれvfredusumとvfwredusumに変更しました。古い名称はエイリアスとして残されています。
  • Masked論理命令は常にマスク解除されることを明確にし、vm=0(マスクされた)のエンコーディングは予約されています。

SonicBOOMに関する調査 (5. BranchTagの構成 2)

BOOMのBranchTagについての解析の続き。分岐命令の予測が外れて、分岐がジャンプする方向に分岐が飛んだものとする。これがこの波形。

f:id:msyksphinz:20210609003611p:plain
f:id:msyksphinz:20210609003651p:plain

この部分のようだ。ずっとBEQでループを繰り返していたが、ループの終了で予測が外れている。

              121368 3 0x00000000800015ee sb      a0, 0(a5)
              121369 3 0x00000000800015f2 addi    a4, sp, 63 x14 0x0000000080024ccf
              121369 3 0x00000000800015f6 c.li    a5, 10 x15 0x000000000000000a
              121370 3 0x00000000800015f8 andi    a4, a4, -64 x14 0x0000000080024cc0
              // この命令の分岐予測が外れる
              121370 3 0x00000000800015fc beq     a0, a5, pc + 18
              // 160eへジャンプ
              121380 3 0x000000008000160e li      a5, 64 x15 0x0000000000000040
              121380 3 0x0000000080001612 c.sd    a5, 0(a4)
              121381 3 0x0000000080001614 c.li    a5, 1 x15 0x0000000000000001
              121381 3 0x0000000080001616 c.sd    a5, 8(a4)
              121382 3 0x0000000080001618 mv      a2, tp x12 0x0000000080005100
              121382 3 0x000000008000161c c.sd    a2, 16(a4)
              121383 3 0x000000008000161e c.sd    a3, 24(a4)
              121423 3 0x0000000080001620 fence
              121437 3 0x0000000080001624 auipc   a5, 0x0 x15 0x0000000080001624
              121442 3 0x0000000080001628 sd      a4, -1572(a5)
              121443 3 0x000000008000162c auipc   a3, 0x0 x13 0x000000008000162c
              121445 3 0x0000000080001630 addi    a3, a3, -1516 x13 0x0000000080001040

もう少し波形を追いかけてみると、命令キャッシュの格納されているフロントエンド側にリダイレクト用の信号が飛んでおり、これに基づいて命令キャッシュアドレスが変更されている。

f:id:msyksphinz:20210609003631p:plain

これを作り出しているのが以下のChiselの実装部分。mispredictで動くようになっている。

  • src/main/scala/exu/core.scala
  } .elsewhen (brupdate.b2.mispredict && !RegNext(rob.io.flush.valid)) {
    val block_pc = AlignPCToBoundary(io.ifu.get_pc(1).pc, icBlockBytes)
    val uop_maybe_pc = block_pc | brupdate.b2.uop.pc_lob
    val npc = uop_maybe_pc + Mux(brupdate.b2.uop.is_rvc || brupdate.b2.uop.edge_inst, 2.U, 4.U)
    val jal_br_target = Wire(UInt(vaddrBitsExtended.W))
    jal_br_target := (uop_maybe_pc.asSInt + brupdate.b2.target_offset +
      (Fill(vaddrBitsExtended-1, brupdate.b2.uop.edge_inst) << 1).asSInt).asUInt
    val bj_addr = Mux(brupdate.b2.cfi_type === CFI_JALR, brupdate.b2.jalr_target, jal_br_target)
    val mispredict_target = Mux(brupdate.b2.pc_sel === PC_PLUS4, npc, bj_addr)
    io.ifu.redirect_val     := true.B
    io.ifu.redirect_pc      := mispredict_target
    io.ifu.redirect_flush   := true.B
    io.ifu.redirect_ftq_idx := brupdate.b2.uop.ftq_idx
    val use_same_ghist = (brupdate.b2.cfi_type === CFI_BR &&

SonicBOOMに関する調査 (4. BranchTagの構成)

BOOMにはBranch Tag(brtag)というのが採用されており、これが一般的なのかは分からないが、どういう機構で動いているのか調査することにした。

Branch Tagはその名の通り分岐命令に対して付与されるタグで、このタグに応じてRenameMapのスナップショットが取られ、分岐予測に失敗した場合にはそのスナップショットから即時リネームIDが復帰される仕組みになっている。

The Rename Stage — RISCV-BOOM documentation

On every branch (or JALR), the Rename Map Tables are snapshotted to allow single-cycle recovery on a branch misprediction.

その様子を観察する。まずはDhrystoneを動かした。RenameMap中にスナップショットを保持するためのFFが確保されている。分岐命令がデコードされると、新たにBranchTagが割り当てられ、その時の最新のRenameMapの値がスナップショットされる。

f:id:msyksphinz:20210608003552p:plain

実際に分岐命令の実行を行い、その結果が予想と異なる場合、分岐予測が外れたことになる。これはRenameMapに即時伝えられ、すぐさまRenameMapがスナップショットに基づいて元に戻される。

f:id:msyksphinz:20210608003607p:plain

つまり、これだけ早くリスタートができるということになるのか?これがフェッチ側にどのように影響しているのか引き続き調査する。

f:id:msyksphinz:20210608003634p:plain