FPGA開発日記

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

Verilatorのコンパイルフローを観察する (3. 内部のデータ変換を追いかける)

Verilatorが内部でどのような方式によりVerilogC++に変換しているのか興味がある。 色々調べてみると、--debugオプションを付けるとデータフローグラフを出力してくれるらしい。

verilator --cc  --debug simple_ff.sv

とりあえず出てきたdotファイルを片っ端から画像に変換する。

for dot in `ls -1 *.dot`
do
    dot -Tjpg -O ${dot}
done

--debugオプションでVerilogC++に変換されるフローが観察できる。

  • 001:リンクされるモジュールファイルをParseする。
- V3ParseImp.cpp:320: Lexing simple_assign.sv
- V3LinkCells.cpp:201:Link Module: MODULE 0x7fffe4eca660 <e3#> {c1ai} u4=0x7fffe4eca7f0  simple_assign  L0 [NONE]
dot -Tpdf -o ~/a.pdf obj_dir/Vsimple_assign_001_linkcells.dot
  • 002:デザインの構造を把握する。
- V3Ast.cpp:1124:     Dumping obj_dir/Vsimple_assign_002_cells.tree
Verilator Tree Dump (format 0x3900) from <e0> to <e94>
     NETLIST 0x7fffe4eb4f40 <e1#> {a0aa}  $root [1ps/1ps]
    1: MODULE 0x7fffe4eca660 <e94#> {c1ai}  simple_assign  L0 [1ps]
    1:2: PORT 0x7fffe4ecb3d0 <e19#> {c3az}  in1
    1:2: VAR 0x7fffe4ecb8d0 <e22#> {c3az} @dt=0@  in1 INPUT PORT
    1:2:1: BASICDTYPE 0x7fffe4ecb4b0 <e21#> {c3ak} @dt=this@(nw1)  logic kwd=logic
    1:2:1:1: RANGE 0x7fffe4ec4f50 <e18#> {c3ar}
    1:2:1:1:2: CONST 0x7fffe4ecb590 <e16#> {c3at} @dt=0x7fffe4ecad20@(G/swu32/3)  ?32?sh4
    1:2:1:1:3: CONST 0x7fffe4ecb720 <e17#> {c3aw} @dt=0x7fffe4ecb190@(G/swu32/1)  ?32?sh0
    1:2: PORT 0x7fffe4ecc5e0 <e50#> {c4az}  in2
    1:2: VAR 0x7fffe4eccdf0 <e49#> {c4az} @dt=0@  in2 INPUT PORT
    1:2:1: BASICDTYPE 0x7fffe4ecc6c0 <e48#> {c4ak} @dt=this@(nw1)  logic kwd=logic
    1:2:1:1: RANGE 0x7fffe4ecca50 <e45#> {c4ar}
    1:2:1:1:2: CONST 0x7fffe4eccb10 <e43#> {c4at} @dt=0x7fffe4ecad20@(G/swu32/3)  ?32?sh4
    1:2:1:1:3: CONST 0x7fffe4eccc80 <e44#> {c4aw} @dt=0x7fffe4ecb190@(G/swu32/1)  ?32?sh0
    1:2: PORT 0x7fffe4ecdb00 <e78#> {c5az}  out
    1:2: VAR 0x7fffe4ece0c0 <e77#> {c5az} @dt=0@  out OUTPUT PORT
    1:2:1: BASICDTYPE 0x7fffe4ecdbe0 <e76#> {c5al} @dt=this@(nw1)  logic kwd=logic
    1:2:1:1: RANGE 0x7fffe4ecdcc0 <e73#> {c5ar}
    1:2:1:1:2: CONST 0x7fffe4ecdd80 <e71#> {c5at} @dt=0x7fffe4ecad20@(G/swu32/3)  ?32?sh4
    1:2:1:1:3: CONST 0x7fffe4ecdf10 <e72#> {c5aw} @dt=0x7fffe4ecb190@(G/swu32/1)  ?32?sh0
    1:2: ASSIGNW 0x7fffe4ece860 <e93#> {c8am} @dt=0@
    1:2:1: AND 0x7fffe4ece7a0 <e91#> {c8as} @dt=0@
    1:2:1:1: PARSEREF 0x7fffe4ece540 <e88#> {c8ao}  in1 [TEXT]
    1:2:1:2: PARSEREF 0x7fffe4ece6c0 <e89#> {c8au}  in2 [TEXT]
    1:2:2: PARSEREF 0x7fffe4ece3c0 <e92#> {c8ai}  out [TEXT]
    3: TYPETABLE 0x7fffe4eb55e0 <e2#> {a0aa}
                detailed  ->  BASICDTYPE 0x7fffe4ecb190 <e13#> {c3aw} @dt=this@(G/swu32/1)  logic [GENERIC] kwd=logic range=[31:0]
                detailed  ->  BASICDTYPE 0x7fffe4ecad20 <e8#> {c3at} @dt=this@(G/swu32/3)  logic [GENERIC] kwd=logic range=[31:0]
    3:1: BASICDTYPE 0x7fffe4ecad20 <e8#> {c3at} @dt=this@(G/swu32/3)  logic [GENERIC] kwd=logic range=[31:0]
    3:1: BASICDTYPE 0x7fffe4ecb190 <e13#> {c3aw} @dt=this@(G/swu32/1)  logic [GENERIC] kwd=logic range=[31:0]
  • 007:これはどうも良く分からない。リンク?
- V3Ast.cpp:1124:     Dumping obj_dir/Vsimple_assign_007_link.tree
// Each module:
//   Look for BEGINs
//      BEGIN(VAR...) -> VAR ... {renamed}
//   FOR -> WHILEs
//
//   Add JumpLabel which branches to after statements within JumpLabel
//      RETURN -> JUMPBLOCK(statements with RETURN changed to JUMPGO, ..., JUMPLABEL)
//      WHILE(... BREAK) -> JUMPBLOCK(WHILE(... statements with BREAK changed to JUMPGO),
//                                    ... JUMPLABEL)
//      WHILE(... CONTINUE) -> WHILE(JUMPBLOCK(... statements with CONTINUE changed to JUMPGO,
//                                    ... JUMPPABEL))
  • 008:linkInc、これも良く分からない。結果は007のリンクと一緒だった。
- V3Ast.cpp:1124:     Dumping obj_dir/Vsimple_assign_008_linkInc.tree- 
// V3LinkInc's Transformations:
//
//      prepost_stmt_visit
//        PREADD/PRESUB
//          Create a temporary __VIncrementX variable, assign the value of
//          the current variable value to it, substitute the current
//          variable with the temporary one in the statement.
//          Increment/decrement the original variable with by the given
//          value.
//        POSTADD/POSTSUB
//          Increment/decrement the current variable by the given value.
//          Create a temporary __VIncrementX variable, assign the value of
//          of the current variable (after the operation) to it. Substitute
//          The original variable with the temporary one in the statement.
//      prepost_non_stmt_visit
//        PREADD/PRESUB/POSTADD/POSTSUB
//          Increment/decrement the current variable by the given value.
//          The order (pre/post) doesn't matter outside statements thus
//          the pre/post operations are treated equally and there is no
//          need for a temporary variable.
  • 010:LinkDot
- V3Ast.cpp:1124:     Dumping obj_dir/Vsimple_assign_010_paramlink.tree
// LinkDot TRANSFORMATIONS:
//      Top-down traversal in LinkDotFindVisitor
//          Cells:
//              Make graph of cell hierarchy
//          Var/Funcs's:
//              Collect all names into symtable under appropriate cell
//      Top-down traversal in LinkDotScopeVisitor
//          Find VarScope versions of signals (well past original link)
//      Top-down traversal in LinkDotParamVisitor
//          Create implicit signals
//      Top-down traversal in LinkDotResolveVisitor
//          VarXRef/Func's:
//              Find appropriate named cell and link to var they reference
  • 012:Width
- V3Ast.cpp:1124:     Dumping obj_dir/Vsimple_assign_012_width.tree
// V3Width's Transformations:
//      Top down traversal:
//          Determine width of sub-expressions
//              width() = # bits upper expression wants, 0 for anything-goes
//              widthUnsized() = # bits for unsized constant, or 0 if it's sized
//              widthMin() = Alternative acceptable width for linting, or width() if sized
//              Determine this subop's width, can be either:
//                  Fixed width X
//                  Unsized, min width X   ('d5 is unsized, min 3 bits.)
//              Pass up:
//                  width() = # bits this expression generates
//                  widthSized() = true if all constants sized, else false
//          Compute size of this expression
//          Lint warn about mismatches
//              If expr size != subop fixed, bad
//              If expr size  < subop unsized minimum, bad
//              If expr size != subop, edit netlist
//                      For == and similar ops, if multibit underneath, add a REDOR
//                      If subop larger, add a EXTRACT
//                      If subop smaller, add a EXTEND
//          Pass size to sub-expressions if required (+/-* etc)
//              FINAL = true.
//              Subexpressions lint and extend as needed
  • 014:Const
- V3Const.cpp:2658:   constifyAllLive:
- V3Ast.cpp:1124:     Dumping obj_dir/Vsimple_assign_014_const.tree
// CONST TRANSFORMATIONS:
//   Call on one node for PARAM values, or netlist for overall constant folding:
//      Bottom up traversal:
//          Attempt to convert operands to constants
//          If operands are constant, replace this node with constant.

Vivado Simulatorを使ってUVMに入門する

私は論理回路の中でもデザインエンジニアなので、UVMが非常に苦手である。というか全く分からない。

ただし分からないと言うばかりではどうしようもないので、Vivado Simulatorを使って多少は勉強しなければなるまい。 UVMはUniversal Verification Methodologyの略で、主に検証で使用されるメソドロジ及びライブラリのセットだ。

Viavdo Simulatorは2019.02からUVMをサポートをしているらしい。まずはVivado Simulatorの環境を構築してみよう。

参考にしたのは以下のサイトだ。これを見ながらVivado Simulatorの環境を作っていった。

sites.google.com

  • test.sv
class test extends uvm_test;
  `uvm_component_utils(test)
  `uvm_new_func
    task run_phase(uvm_phase phase);
      phase.raise_objection(this);
      $display("Hello World\n");
      phase.drop_objection(this);
    endtask
endclass
  • tb_top.sv
module tb_top;
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "test.sv"
initial begin
  run_test();
end
endmodule

で、Makefileを以下のように作成する。

.PHONY: all

all:
        xvlog -sv tb_top.sv -L uvm
        xelab tb_top -timescale 1ns/100ps -L uvm
        xsim tb_top -R --testplusarg "UVM_TESNAME=test"
$ make

一応それっぽく"Helllo World"が表示された。

UVM_INFO /proj/xbuilds/SWIP/2020.2_0711_1805/installs/lin64/Vivado/2020.2/data/system_verilog/uvm_1.2/xlnx_uvm_package.sv(18648) @ 0: reporter [NO_DPI_TSTNAME] UVM_NO_DPI defined--getting UVM_TESTNAME directly, without DPI
UVM_INFO @ 0: reporter [RNTST] Running test test...
UVM_INFO /proj/xbuilds/SWIP/2020.2_0711_1805/installs/lin64/Vivado/2020.2/data/system_verilog/uvm_1.2/xlnx_uvm_package.sv(20867) @ 0: reporter [UVM/COMP/NAMECHECK] This implementation of the component name checks requires DPI to be enabled
Hello World

UVM_INFO /proj/xbuilds/SWIP/2020.2_0711_1805/installs/lin64/Vivado/2020.2/data/system_verilog/uvm_1.2/xlnx_uvm_package.sv(19968) @ 0: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /proj/xbuilds/SWIP/2020.2_0711_1805/installs/lin64/Vivado/2020.2/data/system_verilog/uvm_1.2/xlnx_uvm_package.sv(13673) @ 0: reporter [UVM/REPORT/SERVER] [UVM/RELNOTES]     1

たぶんだが、まずは+UVM_TESTNAME=xxxでテストのためのクラスを指定している。 run_test()を実行しているが、これ自体はおそらくデフォルトで定義されている関数で、run_phase()はその中で呼び出される関数なのだろう。

とりあえず環境自体は出来たので、次はチュートリアル的に何かを実行してみたいかな。

Verilatorのコンパイルフローを観察する (2. 順序回路を生成するまで)

クロックデザインをどのように設計しているのか見てみることにした。以下のようなVerilogファイルをコンパイルして生成されるファイルを観察する。

  • simple_ff.sv
module simple_ff
  (
   input logic          clk,
   input logic [ 4: 0]  in,
   output logic [ 4: 0] out
   );

logic [ 4: 0]           tmp_ff;

always_ff @ (posedge clk) begin
  tmp_ff <= in;
  out <= tmp_ff;
end
void Vsimple_ff::_eval(Vsimple_ff__Syms* __restrict vlSymsp) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_ff::_eval\n"); );
    Vsimple_ff* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    if (((IData)(vlTOPp->clk) & (~ (IData)(vlTOPp->__Vclklast__TOP__clk)))) {
        vlTOPp->_sequent__TOP__1(vlSymsp);
    }
    // Final
    vlTOPp->__Vclklast__TOP__clk = vlTOPp->clk;
}

こんなんでいいのか(笑)。clk信号がタイムスライス中で反転した場合、_sequent__TOP__1が実行される。

VL_INLINE_OPT void Vsimple_ff::_sequent__TOP__1(Vsimple_ff__Syms* __restrict vlSymsp) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_ff::_sequent__TOP__1\n"); );
    Vsimple_ff* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    vlTOPp->out = vlTOPp->simple_ff__DOT__tmp_ff;
    vlTOPp->simple_ff__DOT__tmp_ff = vlTOPp->in;
}

この論理が少し妙だ。これはoutから順に

  • vlTOPp->simple_ff__DOT__tmp_ffからvlTOPp->out

  • vlTOPp->inからvlTOPp->simple_ff__DOT__tmp_ff

となっているがもしこれが逆に配置されていれば普通に信号が通過してinからoutまで一気通貫で実行されてしまう。これはデータフローを解析して、出力に近い側から評価するようになっているのか?

代入の順番を逆にしてもやはり結果は同じだった。容易に想像できるのは、FFを含むデータフローを作って、出力側から追いかけていき代入するフローを作っていくことだ。ソフトウェアパイプラインのような技法に近い。

色々調べてみると、--debugオプションを付けるとデータフローグラフを出力してくれるらしい。

verilator --cc  --debug simple_ff.sv

とりあえず出てきたdotファイルを片っ端から画像に変換する。

for dot in `ls -1 *.dot`
do
    dot -Tjpg -O ${dot}
done

最終的なグラフは以下のようになった。これではまだ良く分からないなあ...

f:id:msyksphinz:20210330001528j:plain

VerilatorのコンパイルバックエンドをClang+LLVMに変更する

VerilatorはSystem Verilog回路を読み込みフロントエンドで処理したのちC++を出力する。 出力したC++ファイルをコンパイルして実際のバイナリに変換するのがバックエンドだが、この時のコンパイラは基本的に自由に指定することができる。

自作CPUのデザインが大きくなってコンパイルが遅いなあ、と思っておりClangに移行したかったのだがマニュアルを見てもやり方が分からずしばらく放置していたのだが、やり方が分かったので変更してみた。

Verilatorコマンド実行時の環境変数ではなく、make実行時のオプションとしてCXX=clang++ LINK=clang++を追加しなければならなかったのだ。 verilated.mk`の中身を読んでやっと理解した。

VERILATOR_OPTS += -MAKEFLAGS "OBJCACHE=ccache CXX=clang++ LINK=clang++"
VERILATOR_OPTS += --compiler clang

 rv64_standard: $(DECODE_FILES) $(FILELIST) libspike_dpi.so
     verilator --top-module $(TOP) -o ../$(TOP)_rv64_standard --Mdir obj_dir_rv64_standard $(VERILATOR_OPTS) ../src/riscv_common_pkg.sv ../src/riscv64_pkg.sv ../src/msrh_standard_conf_pkg.sv  -f $(FILELIST)

-MAKEFLAGSオプションがmake実行時に渡されるオプションらしい。これを修正する。

これでコンパイルすると、おそらく体感速度でかなり高速化されたような気がする。 試しに自作CPUのいろんなコンフィグレーションでコンパイルしてみる。

  • Clang++

    • rv64_small 3:40.81
    • rv64_standard 5:35.10
    • rv64_big 9:43.64
  • GCC

    • rv64_small 5:56.09
    • rv64_standard 8:07.60
    • rv64_big 終了せず
f:id:msyksphinz:20210330004113p:plain

Verilatorのコンパイルフローを観察する (1. 組み合わせ回路を生成するまで)

少し気になってVerilatorの中味を調査している。VerilatorはSystem Verilogのファイルを読み込んでそれをC++に変換しシミュレーションするためのツールだが、VerilogのファイルがどのようなC++ファイルに変換されているのか気になったので見てみることにした。

このモチベーションは、割と巨大なプロジェクトをVerilatorで読み込むと変換すらできなくなってしまうくらいにVerilatorが重たくなってしまうことだ。これの原因が何なのか、原因が分かればラッキーだし、Verilatorのシミュレーションの仕組みについても見てみたい。

例として、以下のような非常にシンプルなデザインを作成した。クロックは存在せず、単純にinからoutへ接続されているだけだ。

module simple_assign
  (
   input logic [ 4: 0]  in,
   output logic [ 4: 0] out
   );

assign out = in;

endmodule // simple_assign

これにWrapperを付けて、Verilatorでコンパイルする。すると、obj_dirに生成C++ファイルとオブジェクトファイルが生成される。

make
verilator --cc --exe  simple_assign.sv -exe tb_simple_assign.cpp
make -C obj_dir -f Vsimple_assign.mk

これの中味を見てみよう。まずはobj_dir/Vsimple_assign.hから。これにはC++クラスが定義されている。

// VL_MODULEはverilated.hに定義されている:
// /// Declare a module, ala SC_MODULE
// #define VL_MODULE(modname) class modname : public VerilatedModule
  VL_MODULE(Vsimple_assign) {
   public:

// VL_IN8とVL_OUT8はverilated.hに定義されている:
// typedef vluint8_t    CData;     ///< Verilated pack data, 1-8 bits
// #define VL_IN8(name, msb, lsb) CData name  ///< Declare input signal, 1-8 bits
// #define VL_OUT8(name, msb, lsb) CData name  ///< Declare output signal, 1-8 bits
     VL_IN8(in,4,0);
     VL_OUT8(out,4,0);

     // INTERNAL VARIABLES
     // Internals; generally not touched by application code
     Vsimple_assign__Syms* __VlSymsp;  // Symbol table

     // CONSTRUCTORS
   private:
     VL_UNCOPYABLE(Vsimple_assign);  ///< Copying not allowed

eval()メソッドはpublicとして公開されており、実体はeval_step()を呼び出している。eval_step()は論理が落ち着くまで(100回程度)回路の評価を繰り返している。

評価の中味はおそらく_eval()の中なので、これを覗いてみよう。

 void Vsimple_assign::eval_step() {
 /* ... 中略 ... */
     do {
         VL_DEBUG_IF(VL_DBG_MSGF("+ Clock loop\n"););
         _eval(vlSymsp);
         if (VL_UNLIKELY(++__VclockLoop > 100)) {
             // About to fail, so enable debug to see what's not settling.
             // Note you must run make with OPT=-DVL_DEBUG for debug prints.
             int __Vsaved_debug = Verilated::debug();
             Verilated::debug(1);
             __Vchange = _change_request(vlSymsp);
             Verilated::debug(__Vsaved_debug);
             VL_FATAL_MT("simple_assign.sv", 1, "",
                 "Verilated model didn't converge\n"
                 "- See DIDNOTCONVERGE in the Verilator manual");
         } else {
             __Vchange = _change_request(vlSymsp);
         }
     } while (VL_UNLIKELY(__Vchange));

_eval()の中では、組み合わせ回路を動かすための記述_combo__TOP__1()が呼び出されている。

 void Vsimple_assign::_eval(Vsimple_assign__Syms* __restrict vlSymsp) {
     VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_assign::_eval\n"); );
     Vsimple_assign* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
     // Body
     vlTOPp->_combo__TOP__1(vlSymsp);
 }

内部では、out変数のポインタに対してin変数のポインタの実体をコピーしている論理が見えた。これがassign)の記述となっている。

 VL_INLINE_OPT void Vsimple_assign::_combo__TOP__1(Vsimple_assign__Syms* __restrict vlSymsp) {
     VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_assign::_combo__TOP__1\n"); );
     Vsimple_assign* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
     // Body
     vlTOPp->out = vlTOPp->in;
 }

ではこれをout = in1 & in2のように変更するとどうなるか。生成されるC++ファイルは以下のように変化する。

 VL_INLINE_OPT void Vsimple_assign::_combo__TOP__1(Vsimple_assign__Syms* __restrict vlSymsp) {
     VL_DEBUG_IF(VL_DBG_MSGF("+    Vsimple_assign::_combo__TOP__1\n"); );
     Vsimple_assign* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
     // Body
     vlTOPp->out = ((IData)(vlTOPp->in1) & (IData)(vlTOPp->in2));
 }

順不同に格納されたエントリの順番を保持するAge Matrix回路について (SystemVerilogで書いてみる)

Age Matrixの続き。理解のために実際に自分でも書いてみることにした。

モジュールとしては SIZEエントリを保持するバッファを作ってみた。

  • push
    • Valid
    • Pushする場所を示すインデックス(OH)
    • Pushするデータ
  • pop
    • Valid
    • Popする場所を示すインデックス(OH)
 logic [SIZE-1: 0]                 r_entry_valid;   // エントリが有効であることを示す
 logic [31: 0]                     r_entry [SIZE];   // エントリの値
 logic [SIZE-1:0]                  r_age_matrix[SIZE];   // Age Matrixのレジスタ
 logic [SIZE-1: 0]                 w_age_matrix_or;   // Age MatrixのOR値
 logic [SIZE-1: 0]                 w_grant;  // Validに対してGrantを取っているポート(OH)

まずAge Matrixの論理だが、

  • Push時:Pushする自分のエントリのAge Matrixに対して、これまでに格納されているエントリの位置を示すビットのORを格納する。
  • Pop時:すべてのエントリのAge Matrixに対して自分の位置をしめすビットを0に落とす

これを記述したのが以下となる(詳細に検証していないので細かいところで間違ってるかも)。

 bit_or #(.WIDTH(SIZE), .WORDS(SIZE)) age_matrix_or (.i_data(r_age_matrix), .o_selected(w_age_matrix_or));

 generate for (genvar i = 0; i < SIZE; i++) begin : e_loop
   logic [SIZE-1: 0] w_age_matrix_in;
   assign w_age_matrix_in = {SIZE{i_push & i_push_idx_oh[i]}} & (w_age_matrix_or | (1 << i)) |
                            ~({SIZE{i_pop}} & i_pop_idx_oh) & r_age_matrix[i];
   always_ff @ (posedge i_clk, negedge i_reset_n) begin
     if (!i_reset_n) begin
       r_age_matrix[i] <= {SIZE{1'b0}};
     end else begin
       r_age_matrix[i] <= (i_push & i_push_idx_oh[i] | i_pop) ? w_age_matrix_in : r_age_matrix[i];
     end
   end

i_push時にはi_push_idx_oh[i]に対して(つまり自分)のみw_age_matrix_orの値と自分を示すビット(1 << i)をORして格納する。 i_pop時には、すべてのビットに対してr_age_matrix[i]から自分を示すビットを消して格納する。

grant信号は、とりあえず自分以外のビットが立っていればGrantしないという論理にする。

 generate for (genvar i = 0; i < SIZE; i++) begin : e_loop
   assign w_grant[i] = r_entry_valid[i] & ~|(r_age_matrix[i] & ~(1 << i));
 end
 endgenerate

とりあえずテストベンチを作ってシミュレーションしてみる。

f:id:msyksphinz:20210328000759p:plain

なんとなく上手く行っている気がする。

順不同に格納されたエントリの順番を保持するAge Matrix回路について

とある事情でAge Matrix回路について調べていた。 存在は何となく知っていたし、エントリのLiveness管理に必要だという理解はあったものの、どういう回路で動いているのか全く理解していなかったのでこの際頑張って勉強してみることにした。

そもそもの目的は、例えばCPUのInstruction Issue Queueは複数の命令を保持しておくためのQueueであるが、依存関係の解決した命令から順番に発行することができる。 ただし何も考えずに依存関係の解決したもの順に出せばよいということではなく、「同時に複数発行できる命令が存在している場合、先に入ってきたエントリから順番に発行してほしい」ということになる。 例えば条件的にエントリAとエントリBが同時に発行可能になったとしても、Aの方が先にエントリに格納された(古い命令)である場合、Aを優先的に発行したい。 エントリは空いている場所にとりあえず命令を格納するので、エントリ番号的にA>Bである場合単純にエントリ順に発行するとBが先に発行されてしまう。 これを解決するのがAge Matrixという訳だ。

Age Matrixの説明はいくつかの場所で載っているが、ググってそれっぽい解説が無いので別の呼び名が通例なのだろうか?とりあえず以下の論文が出てくる。

  • Matrix Scheduler Reloaded

https://dl.acm.org/doi/10.1145/1250662.1250704

ちょうどMatrix Reloadedが公開された年代に近いので、タイトルは駄洒落っぽい。いつかMatrix Scheduler Revolutionsを出して欲しい。

以下のような図が載っていて、なるほど何となくその意味は理解できるような気がする。

f:id:msyksphinz:20210326232847p:plain

Nエントリが用意されている場合、Inserterは空いている場所ならどこにでも値を格納できるものとする。 まあ回路設計の場合は単純なLSB抽出器でもっと番号の小さい、空いている場所へ格納することだろう。 この際、 N \times Nマトリックスを用意しておき、マトリックス i行目は、「エントリ i」が格納される前までに格納されていたエントリ」の情報が格納されている。 つまり、自分よりも古いエントリが誰なのかが、マトリックスの行をサーチすることで分かるようになる。 「エントリ iがエントリ内でもっと古い」場合、 i行をサーチすると自分を示すビット( iビット目)のみが1になっているというように理解すれば良かろう。

エントリを挿入するときは上記の通りで、エントリを削除する際は自分のエントリに相当するマトリックスの横の行と、縦の行もすべて0に設定する。 これにより、自分の次に古いエントリは自分の情報が削除され、最も古いエントリとしてみなされるようになる。

説明を書いても意味不明なので、 6\times 6のAge Matrixを作って実際にシミュレーションしてみる。

初期状態は何も格納されていないとする。

f:id:msyksphinz:20210326233629p:plain

これに対して、エントリ0から順番にエントリに値が格納されていったものとする。Age Matrixは以下のようになる。エントリのインデックスは右から左に昇順に並んでいるものとする([6:0][6:0]のイメージ)。

f:id:msyksphinz:20210326234005p:plain

Age Matrixにおいて1が付いているのは、「自分か、自分よりも前に格納されたエントリの場所」を意味している。 エントリ2はエントリ1とエントリ0の後に格納されたので、Age Matrixの2行目はビット0とビット1(と自分を示すビット2)が1に設定されている。 さらに、エントリ1はエントリ0の次に格納されたので、Age Matrixの1行目はビット0(と自分を示すビット1)が1に設定されている。

ここで、すべてのエントリがReady信号を出したものとする。ArbitrationにてGrantを出せるのは1つのエントリのみなのだが、Grantを出せるのは、「自分を示すビットのみが1に設定されているエントリのみ」とする。 この例だとエントリ0が相当する。エントリ0は自分を示すビット0のみが1に設定されており、それ以外は0となっている。 一方でエントリ0以外は、自分を示すビット以外のビット(エントリ0を示すビット0はすべて)が1になっており、Grantは得られない。

f:id:msyksphinz:20210326234355p:plain

ここでエントリ3が削除されたものとする。エントリ3は自分のAge Matrixの行を0に設定するとともに、自分に相当する列ビット(3ビット目)もすべて0に設定する。 エントリ0がGrantを得られ続けていることは変わらない。

f:id:msyksphinz:20210326234758p:plain

次にエントリ0が削除されたものとする。Age Matrixの0行目が0に設定されるとともに、列ビット0もすべて0に設定される。 これにより、次にGrantが得られるのは自分自身を示すビットのみが1となっている、エントリ1になる。

f:id:msyksphinz:20210326234927p:plain

さらにエントリ1を削除する。エントリ2が次にGrantを得られるようになる。

f:id:msyksphinz:20210326235019p:plain

ここでエントリ2が削除され、さらにエントリ1に新しい値が挿入されたらどうなるか。 エントリ1は生き残っている「エントリ4」と「エントリ5」のビットを引き継いでAge Matrixの1行目に対して1,4,5ビット目を1に設定する。 この状態ではエントリ1にGrant権限は得られず、エントリ4がGrant権限を取得する。

f:id:msyksphinz:20210327000326p:plain

さらにエントリ0に新しい値が挿入されたとしても、生き残っているエントリ4、エントリ5、エントリ1のビットを引き継ぐためAge Matrixの0行目に対してビット0、1、4、5を1に設定する。 やはりGrant権限は得られず、エントリ4がGrant権限を取得する。

f:id:msyksphinz:20210327000506p:plain

このように、「自分のエントリはどのエントリの後に格納されたのか」の情報を常に保持することで、どのような順番にエントリが格納されたとしても順序を把握できるようにしている。 これがAge Matrixの仕組みという訳だ。勉強になった。