Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

Dynamic Scheduler版のVerilatorの中を調べる(その4)

@Vengineerの戯言 : Twitter SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

はじめに

Dynamic Scheduler版 Verilatorの中を調べる、その4。今回は、fork/join がどのように実装されているのかをみていきます。

Verilog HDLコード

Verilog HDLコード (examples/fork/fork.v) を以下に示します。initial 文の中で、fork/join, fork/join_any, fork/join_none のテストを行っています。fork/join の場合では実行したすべての threadが終了するまで待ちます。fork/join_any の場合は実行したどれからのthreadが終了するまで待ちます。fork/join_none の場合は どの thread の終了を待たずに次を実行します。

module t;
   event cont;

   initial begin
      fork
         begin
            $write("forked process\n");
         end
         begin
            $write("forked process\n");
         end
         begin
            $write("forked process\n");
         end
      join
      $write("join in main process\n");
      $write("==========================\n");
      fork
         begin
            $write("forked process 1\n");
         end
         begin
            @cont;
            $write("forked process 2\n");
            ->cont;
         end
      join_any
      #1;
      $write("join_any in main process\n");
      ->cont;
      @cont;
      $write("==========================\n");
      fork
      begin
         #1;
         $write("forked process\n");
         $write("*-* All Finished *-*\n");
         $finish;
      end
      join_none
      $write("join_none in main process\n");
   end
endmodule

fork/join の場合はそのままなので説明は省きます。fork/join_anyとfork/join_noneの説明をします。

下記に、fork/join_anyの場合のVerilog HDLコードを抜き出しました。最初のブロックは $write システムタスクを実行するだけなので直ぐ(時間0)で終了します。2番目のブロックは @cont にて event 待ちをしています。fork/join_any なので最初のブロックが終了すると、join_any の後の #1 以降が実行されます。#1; の後に、$write("join_any in main process\n"); が実行され、->cont にて event を発生させます。ここで2番目のブロック内の @cont で待っている部分から $write(forked process 2\n"); が実行され、->cont を実行後終了しまう。->cont に対して、@cont にて event 待ちができ、この部分が終了します。

      fork
         begin
            $write("forked process 1\n");
         end
         begin
            @cont;
            $write("forked process 2\n");
            ->cont;
         end
      join_any
      #1;
      $write("join_any in main process\n");
      ->cont;
      @cont;

下記に、fork/join_noneの場合のVerilog HDLコードを抜き出しました。ブロック分が終了することなしに$write("join_none in main process\n"); が実行されます。その後に、#1; の後の $write("forked proces\n");、$write("- All Finished -\n");、$finish; が実行されます。

      fork
      begin
         #1;
         $write("forked process\n");
         $write("*-* All Finished *-*\n");
         $finish;
      end
      join_none
      $write("join_none in main process\n");

生成されたC++コード

initial 文は、Vtop__Slow.cpp の _initial__TOP__1 メソッドです。fork/join 内の各ブロックは、thread にて実行されています。

fork/join

fork/join の場合は、次のようなコードになります。3つのブロックが thread になり、3つのスレッドが終了されるまで、join.await() で待っています。 VerilatedThread::Join join(*self, 3) で 3つの thread が終了するまで待つようにしています。

void Vtop::_initial__TOP__1(Vtop__Syms* __restrict vlSymsp, VerilatedThread* self) {
    VL_DEBUG_IF(VL_DBG_MSGF("+    Vtop::_initial__TOP__1\n"); );
    Vtop* const __restrict vlTOPp VL_ATTR_UNUSED = vlSymsp->TOPp;
    // Body
    {
        VerilatedThread::Join join(*self, 3);
        thread_pool.run_once([vlSymsp, vlTOPp, &join](VerilatedThread* self) mutable {
                VL_WRITEF("forked process\n");
                join.joined();
        });
        thread_pool.run_once([vlSymsp, vlTOPp, &join](VerilatedThread* self) mutable {
                VL_WRITEF("forked process\n");
                join.joined();
        });
        thread_pool.run_once([vlSymsp, vlTOPp, &join](VerilatedThread* self) mutable {
                VL_WRITEF("forked process\n");
                join.joined();
        });
        join.await();
        if (self->should_exit()) return;
    }
    VL_WRITEF("join in main process\n==========================\n");

fork/join_any

fork/join_anyの場合は、 auto join = std::make_shared<VerilatedThread::Join>(*self, 1) にて、1つの thread が終了するまで待つように設定しているので、2つの thread のいづれかが終了すると、join->await() から抜けます。

    {
        auto join = std::make_shared<VerilatedThread::Join>(*self, 1);
        thread_pool.run_once([vlSymsp, vlTOPp, join](VerilatedThread* self) mutable {
                VL_WRITEF("forked process 1\n");
                join->joined();
        });
        thread_pool.run_once([vlSymsp, vlTOPp, join](VerilatedThread* self) mutable {
                /* [@ statement] */
                {
                    CData __Vtc__tmp0 = vlTOPp->t__DOT__cont;
                    vlTOPp->t__DOT__cont.assign_no_notify(0);
                    self->wait_until([&__Vtc__tmp0](auto&& v) -> bool {
                            bool __Vtc__res = std::get<0>(v);
                            if (!__Vtc__res) {
                                __Vtc__tmp0 = std::get<0>(v);
                            }
                            return __Vtc__res;
                        }, vlTOPp->t__DOT__cont);
                }
                if (self->should_exit()) return;
                VL_WRITEF("forked process 2\n");
                /* [ -> statement ] */
                vlTOPp->t__DOT__cont = 1;
                join->joined();
        });
        join->await();
        if (self->should_exit()) return;
    }
    self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
    if (self->should_exit()) return;
    VL_WRITEF("join_any in main process\n");
    /* [ -> statement ] */
    vlTOPp->t__DOT__cont = 1;
    /* [@ statement] */
    {
        CData __Vtc__tmp0 = vlTOPp->t__DOT__cont;
        vlTOPp->t__DOT__cont.assign_no_notify(0);
        self->wait_until([&__Vtc__tmp0](auto&& v) -> bool {
                bool __Vtc__res = std::get<0>(v);
                if (!__Vtc__res) {
                    __Vtc__tmp0 = std::get<0>(v);
                }
                return __Vtc__res;
            }, vlTOPp->t__DOT__cont);
    }
    if (self->should_exit()) return;
    VL_WRITEF("==========================\n");

fork/join_none

fork/join_noneの場合は、ブロックを待つ必要がないので thread は起動されたままです。wait_for_time メソッドで VL_TIME_Q() + 1U まで待って、VL_WRITEFマクロでメッセージを出力し、VL_FINISH_MTマクロにてシミュレーションを終了しています。

    thread_pool.run_once([=](VerilatedThread* self) mutable {
            self->wait_for_time(vlSymsp, VL_TIME_Q() + 1U);
            if (self->should_exit()) return;
            VL_WRITEF("forked process\n*-* All Finished *-*\n");
            VL_FINISH_MT("/home/vengineer/home/verilator/verilator-dynamic-scheduler-examples/examples/fork/fork.sv", 38, "");
            return;
    });
    VL_WRITEF("join_none in main process\n");
}

おわりに

今回は、fork/join がどのようにC++として生成されるかをみてみました。

とりあえず、Dynamic Scheduler版 Verilator の中を調べるは、今回で終了です。