@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 の中を調べるは、今回で終了です。