ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Chisel3.4.0のリリースノートを確認した(2) - naming & prefix

スポンサーリンク

Chisel3.4.0の変更点確認の2回目。今回からは、気になった機能を確認していく。 まず最初はChisel3.4.0の目玉機能である、コンパイラプラグインとPrefixingによる、命名規則の改善について。

(#1448) Improved Chisel Naming via Compiler Plugins + Prefixing

概要については、前回の記事でざっとまとめたものを転載。

今回の目玉機能。コンパイラプラグインprefixというメソッドが導入された。 コンパイラプラグインによって、Chiselのコード中の変数名の多くが、そのままRTLの信号名として使用されるようになった。 またprefixメソッドを使用することで、信号名に任意のプリフィックスの付与を行い、RTLの可読性を向上させることができるようになった。

PRに載っている効果は次の通りで、16000あった"anonymous wires"が1400まで減ったとのこと。

  • Just auto-naming: reduced number of Chisel source locations which generated anonymous registers from 95 to 19, and anonymous wires > from 19220 to 16073 (16% decrease in bad names).
  • Auto-naming + prefixing of temporaries: further reduction of anonymous wires from 16073 to 10880.
  • Auto-naming + auto-prefixing + added prefixing to one file (Monitor.scala) reduces 10880 -> 1395 anonymous wires.

ということで、なんだか凄い改善が見られている気がする。 この記事では、次の内容を確認してみようと思う

  1. Chisel 3.3.x → Chisel 3.4.0
  2. Chisel 3.4.0 でコンパイラプラグインを有効にする
  3. オブジェクトprefixの効果

Chisel 3.3.x → Chisel 3.4.0

この"Just auto-naming"というのは、ただ単にChiselのバージョンを3.3から3.4に切り替えるだけで得ることが出来る効果になる。 なので、生成されるRTLの可読性を少しでも上げたいなー、、と考えている人は、すぐにでもChiselのバージョンを上げると、少し幸せになれるかも。

この効果を確認するには、そこそこの規模のデザインでChisel3.3/3.4でRTLを生成してみるのが手っ取り早いので、以前に作成したオレオレRISC-Vのコードを使って実験をしてみた。

比較したのはdirvの中のIduモジュール。変化の大きい部分をキャプチャしたものを以下に掲載。全部の結果は長いのでgistのリンクを次のコンパイラプラグインのところに貼ってあります。

f:id:diningyo-kpuku-jougeki:20201123211440j:plain

コンパイラプラグインを有効にする

最初やり方がイマイチ分からなかったのだが、このコンパイラプラグインbuild.sbtに次の記述を追加すると有効に出来る。

lazy val chiselPluginLib = "edu.berkeley.cs" % "chisel3-plugin" % chiselVersion cross CrossVersion.full
addCompilerPlugin(chiselPluginLib)

コンパイラプラグインが有効になっているかどうかはsbtシェルで次のコマンドを実行すれば確認可能。有効になっていればchisel3-pluginが表示される。

IJ]sbt:dirv> show Compile / scalacOptions
[info] * -Xsource:2.11
[info] * -Xplugin:/home/diningyo/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scalamacros/paradise_2.12.10/2.1.1/paradise_2.12.10-2.1.1.jar
[info] * -Xplugin:/home/diningyo/.cache/coursier/v1/https/repo1.maven.org/maven2/edu/berkeley/cs/chisel3-plugin_2.12.10/3.4.0/chisel3-plugin_2.12.10-3.4.0.jar
[success] Total time: 0 s, completed 2020/11/23 20:55:21

全部の結果については、以下のgistのリンクをどうぞ。選択したモジュールがイマイチな例だった、、、というのもあり、あまり劇的な感じではなかった。。 それでもメソッド内部のローカルな変数として宣言したものがきちんとRTLに反映されており、効果は確認することができた。

  def mulInstDecode(): Unit = {
    val m = funct7(0)
    mul.get := aluReg && m && (funct3 === "b000".U)
  wire  m = inst_funct7[0]; // @[Idu.scala 286:19]
  wire  _inst_mul_T = inst_aluReg & m; // @[Idu.scala 287:23]

prefixオブジェクト

PRの内容を読んだ自分の理解としては、ここでのprefixingというのが、今回Chisel 3.4.0で追加されたprefixオブジェクトのことのハズ。

使い方はシンプルで、次のようにprefixオブジェクトの(applyの)引数に付与したいprefixをStringで与えるだけ。

  def apply[T](name: String)(f: => T): T = {

とりあえず、簡単なコードで効果を確認してみる。

class TestBundle extends Bundle {
  val bool = Bool()
  val uint = UInt(8.W)
  val sint = SInt(8.W)
}

class EgPrefix(usePrefix: Boolean) extends Module {
  val io = IO(new Bundle {
    val in = Input(new TestBundle)
    val out = Output(new TestBundle)
  })

  if (usePrefix) {
    // prefixに"r_"を指定
    io.out := prefix("r") { RegNext(io.in) }
  } else {
    io.out := RegNext(io.in)
  }
}

object Elaborate extends App {
  println(new (chisel3.stage.ChiselStage).emitVerilog(new EgPrefix(true)))
  println(new (chisel3.stage.ChiselStage).emitVerilog(new EgPrefix(false)))
}

ここでは次の3種類で比較。

  1. Chisel 3.3.xで生成したRTL
  2. Chisel 3.4.0 && usePrefix == trueで生成したRTL
  3. Chisel 3.4.0 && usePrefix == falseで生成したRTL

  4. Chisel 3.3.xで生成したRTL

module EgPrefix(
  input        clock,
  input        reset,
  input        io_in_bool,
  input  [7:0] io_in_uint,
  input  [7:0] io_in_sint,
  output       io_out_bool,
  output [7:0] io_out_uint,
  output [7:0] io_out_sint
);
  // RegNextの部分が_T_xxxになる
  reg  _T_bool; // @[EgPrefix.scala 23:20]
  reg [7:0] _T_uint; // @[EgPrefix.scala 23:20]
  reg [7:0] _T_sint; // @[EgPrefix.scala 23:20]
  assign io_out_bool = _T_bool; // @[EgPrefix.scala 23:10]
  assign io_out_uint = _T_uint; // @[EgPrefix.scala 23:10]
  assign io_out_sint = _T_sint; // @[EgPrefix.scala 23:10]
  always @(posedge clock) begin
    _T_bool <= io_in_bool;
    _T_uint <= io_in_uint;
    _T_sint <= io_in_sint;
  end
endmodule
  • usePrefix == falseで生成したRTL
module EgPrefix(
  input        clock,
  input        reset,
  input        io_in_bool,
  input  [7:0] io_in_uint,
  input  [7:0] io_in_sint,
  output       io_out_bool,
  output [7:0] io_out_uint,
  output [7:0] io_out_sint
);
  // Chisel 3.4.0では_TがREGに変化する
  reg  io_out_REG_bool; // @[EgPrefix.scala 23:20]
  reg [7:0] io_out_REG_uint; // @[EgPrefix.scala 23:20]
  reg [7:0] io_out_REG_sint; // @[EgPrefix.scala 23:20]
  assign io_out_bool = io_out_REG_bool; // @[EgPrefix.scala 23:10]
  assign io_out_uint = io_out_REG_uint; // @[EgPrefix.scala 23:10]
  assign io_out_sint = io_out_REG_sint; // @[EgPrefix.scala 23:10]
  always @(posedge clock) begin
    io_out_REG_bool <= io_in_bool; // @[EgPrefix.scala 23:20]
    io_out_REG_uint <= io_in_uint; // @[EgPrefix.scala 23:20]
    io_out_REG_sint <= io_in_sint; // @[EgPrefix.scala 23:20]
  end
endmodule
  • usePrefix == trueで生成したRTL
module EgPrefix(
  input        clock,
  input        reset,
  input        io_in_bool,
  input  [7:0] io_in_uint,
  input  [7:0] io_in_sint,
  output       io_out_bool,
  output [7:0] io_out_uint,
  output [7:0] io_out_sint
);
  // prefixを使用すると指定したプリフィックス"r_"がついて
  // r_REGに変化する
  reg  io_out_r_REG_bool; // @[EgPrefix.scala 24:34]
  reg [7:0] io_out_r_REG_uint; // @[EgPrefix.scala 24:34]
  reg [7:0] io_out_r_REG_sint; // @[EgPrefix.scala 24:34]
  assign io_out_bool = io_out_r_REG_bool; // @[EgPrefix.scala 24:10]
  assign io_out_uint = io_out_r_REG_uint; // @[EgPrefix.scala 24:10]
  assign io_out_sint = io_out_r_REG_sint; // @[EgPrefix.scala 24:10]
  always @(posedge clock) begin
    io_out_r_REG_bool <= io_in_bool; // @[EgPrefix.scala 24:34]
    io_out_r_REG_uint <= io_in_uint; // @[EgPrefix.scala 24:34]
    io_out_r_REG_sint <= io_in_sint; // @[EgPrefix.scala 24:34]
  end
endmodule

余談だけど、Chisel 3.4.0からRTLの構成に若干の変更が入ったようで、RTLの可読性を下げている理由の1つであったランダマイズの処理がモジュールの最後に位置するようになった。
先程のRTLを全部載せると次のような感じ。

module EgPrefix(
  input        clock,
  input        reset,
  input        io_in_bool,
  input  [7:0] io_in_uint,
  input  [7:0] io_in_sint,
  output       io_out_bool,
  output [7:0] io_out_uint,
  output [7:0] io_out_sint
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
  reg [31:0] _RAND_1;
  reg [31:0] _RAND_2;
`endif // RANDOMIZE_REG_INIT
  reg  io_out_REG_bool; // @[EgPrefix.scala 22:22]
  reg [7:0] io_out_REG_uint; // @[EgPrefix.scala 22:22]
  reg [7:0] io_out_REG_sint; // @[EgPrefix.scala 22:22]
  assign io_out_bool = io_out_REG_bool; // @[EgPrefix.scala 22:12]
  assign io_out_uint = io_out_REG_uint; // @[EgPrefix.scala 22:12]
  assign io_out_sint = io_out_REG_sint; // @[EgPrefix.scala 22:12]
  // レジスタの実装が先に来るように変更されている。
  always @(posedge clock) begin
    io_out_REG_bool <= io_in_bool; // @[EgPrefix.scala 22:22]
    io_out_REG_uint <= io_in_uint; // @[EgPrefix.scala 22:22]
    io_out_REG_sint <= io_in_sint; // @[EgPrefix.scala 22:22]
  end
// このifdefブロックが最後に来ている
// Register and memory initialization
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
`ifdef RANDOMIZE_MEM_INIT
  integer initvar;
`endif
`ifndef SYNTHESIS
`ifdef FIRRTL_BEFORE_INITIAL
`FIRRTL_BEFORE_INITIAL
`endif
initial begin
  `ifdef RANDOMIZE
    `ifdef INIT_RANDOM
      `INIT_RANDOM
    `endif
    `ifndef VERILATOR
      `ifdef RANDOMIZE_DELAY
        #`RANDOMIZE_DELAY begin end
      `else
        #0.002 begin end
      `endif
    `endif
`ifdef RANDOMIZE_REG_INIT
  _RAND_0 = {1{`RANDOM}};
  io_out_REG_bool = _RAND_0[0:0];
  _RAND_1 = {1{`RANDOM}};
  io_out_REG_uint = _RAND_1[7:0];
  _RAND_2 = {1{`RANDOM}};
  io_out_REG_sint = _RAND_2[7:0];
`endif // RANDOMIZE_REG_INIT
  `endif // RANDOMIZE
end // initial
`ifdef FIRRTL_AFTER_INITIAL
`FIRRTL_AFTER_INITIAL
`endif
`endif // SYNTHESIS
endmodule

このprefixの使いどころについては、Chisel3.4.0のリリース時に追加されているドキュメントに次のような記載がある。

If the plugin is enabled, these signals could be intermediate values which are consumed by either assertions or when predicates. In these cases, the compiler plugin often can't find a good prefix for the generated intermediate signals. We recommend you manually insert calls to prefix to fix these cases. We did this to Rocket Chip and saw huge benefits!

ということで「プラグインを入れたけど、まだ_T_xxが残っている場合」にピンポイントでprefixを指定して使うのが良さそう。 さきほど紹介したIduモジュールにも_T_1といった信号が含まれているので、その信号に対してprefixを適用して確認してみる。 その該当箇所は次のChselコード中のadd(!arith)

//
  def aluInstDecode(): Unit = {
    // register
    aluReg := opcode === "b0110011".U
    add := (funct3 === "b000".U) && aluReg && (!arith)

この(!arith)はRTLになると、次のような信号に変換されている。

  wire  _inst_add_T_2 = ~inst_arith; // @[Idu.scala 258:48]

この(!arith)prefixを使って次のように変更して、RTLを生成した結果は次のように指定したプリフィックスが適用された。

  • prefixを適用したコード
  def aluInstDecode(): Unit = {
    // register
    aluReg := opcode === "b0110011".U
    add := (funct3 === "b000".U) && aluReg &&
      prefix("NOT_arith") { (!arith) }
  • 生成したRTL
  wire  _inst_add_NOT_arith_T = ~inst_arith; // @[Idu.scala 259:70]

noPrefix もある

逆の処理を行うオブジェクトnoPrefixもChisel3.4.0で追加された。使い方は次のようにnoPrefixのブロック式で囲うだけ。

noPrefix {
  <prefixをつけたくない実装>
}

あんまり良い例ではないけど、効果が確認できたのがdirvのCSRの実装部分の次のコード。

// Csrf.scala L.387
  // address decode
  val csrSelBits = Cat(CSR.csrRegAddrs.reverse.map(i => i.asUInt() === inst.getCsr ))

このコードではcsrRegAddrsに格納されているCSRのアドレスをmapを使ってinstに含まれるアドレスと比較している。

このコードからRTLを生成すると、次のようになる。

  wire [11:0] _csrSelBits_T = {io_inst_funct7,io_inst_rs2}; // @[Cat.scala 29:58]
  wire  csrSelBits_right_right_right_right = 12'hf14 == _csrSelBits_T; // @[Csrf.scala 387:63]
  wire  csrSelBits_right_right_right_left = 12'hf13 == _csrSelBits_T; // @[Csrf.scala 387:63]
  wire  csrSelBits_right_right_left_right = 12'hf12 == _csrSelBits_T; // @[Csrf.scala 387:63]
  wire  csrSelBits_right_right_left_left = 12'hf11 == _csrSelBits_T; // @[Csrf.scala 387:63]
  wire  csrSelBits_right_left_right_right = 12'h344 == _csrSelBits_T; // @[Csrf.scala 387:63]
  wire  csrSelBits_right_left_right_left = 12'h343 == _csrSelBits_T; // @[Csrf.scala 387:63]

で、このコードに対してnoPrefixを適用すると、次のように_csrSelBitsプリフィックスとして扱われて、それが丸々削除される結果となった。

  • Chisel
  val csrSelBits = noPrefix { Cat(CSR.csrRegAddrs.reverse.map(i => i.asUInt() === inst.getCsr )) }
  • RTL
  wire [11:0] _T = {io_inst_funct7,io_inst_rs2}; // @[Cat.scala 29:58]
  wire  right_right_right_right = 12'hf14 == _T; // @[Csrf.scala 389:79]
  wire  right_right_right_left = 12'hf13 == _T; // @[Csrf.scala 389:79]
  wire  right_right_left_right = 12'hf12 == _T; // @[Csrf.scala 389:79]
  wire  right_right_left_left = 12'hf11 == _T; // @[Csrf.scala 389:79]
  wire  right_left_right_right = 12'h344 == _T; // @[Csrf.scala 389:79]
  wire  right_left_right_left = 12'h343 == _T; // @[Csrf.scala 389:79]

ということで、Chisel3.4.0の目玉機能と思われる可読性向上のお話でした。