llvm / llvm/circt

Synthetic/Generated-aware Optimizations

Open
#851 6 comments 0 reactions 1 assignee View on GitHub

@prithayan is already working on this.

Since Apr 1, 2021.

enhancement ExportVerilog FIRRTL
Dominant language
C++
Stars
2.2k
Forks
524
Avg merge
3d 2h
Merged PRs (30d)
46

Description

Hardware languages that compile to Verilog and their compilers have three implicit attributes associated with them:

  1. User-specified things
  2. Temporaries generated in the front-end, e.g., Chisel
  3. Synthetics generated in the middle-end, e.g., some llvm/circt optimization pass

User-specified things should be preserved in the output language (e.g., Verilog, VHDL, or SystemC) by default. Anything which is a temporary or a synthetic should be a candidate for any optimization available, e.g., common subexpression elimintation/global value numbering or dead code elimination.

Note: Temporaries and synthetics may be the same thing from the view of a mid-end.

I'm proposing adding this notion of "user specified"/"static"/"public" and "temporary"/"synthetic" in the IR to differentiate between things that are not optimization candidates vs. things that are optimization candidates.

This could be implemented as an attribute on specific ops and be used as a gate on optimizations. (Or: there is an optimization option which will respect this attribute like, -fpreserve-public.)

Note: Even if something is user-specified, it could be optimized around, e.g., constants could propagate through it.

Chisel/Scala FIRRTL Compiler-specific Details

Chisel emits lots of temporaries due to its software architecture. When you write something like:

/* Add 2 to "a" and connect the result to "b" */
b := a + 1.U + 1.U

Chisel is walking things in the order of Scala operations and generating temporaries along the way. You can think of this as analyzing this like so:

b.connect(a.add(1.U).add(1.U))

Each addition generates a new temporary and that temporary is connected to b.

Each temporary that Chisel generates gets a name like _T(_<int>)? producing temporaries of increasing index: [_T, _T_0, _T_1, ...]. A recent version of Chisel produces better names where temporaries are associated with a prefix, e.g., [_foo, _foo_0, _foo_1, ...].

The Scala FIRRTL Compiler may generate synthetic wires/nodes without a name using the _GEN(_<int>)? pattern like:[_GEN, _GEN_0, _GEN_1, ...].

For the purposes of integrating directly with Chisel or the Scala FIRRTL Compiler, this naming convention can be exploited to stringly infer what is a legal optimization candidate.

Example 1

Consider the following Chisel code that adds 2 to some input:

class Foo extends RawModule {
  val a = IO(Input(UInt(1.W)))
  val b = IO(Output(UInt(3.W)))
  
  /** Add 2 to input "a" and connect this to output "b" */
  b := a + 1.U + 1.U
}

This produces the following FIRRTL IR:

circuit Foo :
  module Foo :
    input a : UInt<1>
    output b : UInt<3>

    node _b_T = add(a, UInt<1>("h1")) @[main.scala 11:10]
    node _b_T_1 = tail(_b_T, 1) @[main.scala 11:10]
    node _b_T_2 = add(_b_T_1, UInt<1>("h1")) @[main.scala 11:16]
    node _b_T_3 = tail(_b_T_2, 1) @[main.scala 11:16]
    b <= _b_T_3 @[main.scala 11:5]

The ideal Verilog that should be produced is something with everything inlined:

module Foo(
  input        a,
  output [3:0] b);
  assign b = a + 1'h1 + 1'h1;
endmodule

llvm/circt already generates something close to this. The Scala FIRRTL compiler emits temporaries.

Example 2

Alternatively, if the user writes:

class Bar extends RawModule {
  val a = IO(Input(UInt(1.W)))
  val b = IO(Output(UInt(3.W)))

  val x = a + 1.U
  val y = x + 1.U

  b := y
}

This produces the following FIRRTL IR:

circuit Bar :
  module Bar :
    input a : UInt<1>
    output b : UInt<3>

    node _x_T = add(a, UInt<1>("h1")) @[main.scala 20:13]
    node x = tail(_x_T, 1) @[main.scala 20:13]
    node _y_T = add(x, UInt<1>("h1")) @[main.scala 21:13]
    node y = tail(_y_T, 1) @[main.scala 21:13]
    b <= y @[main.scala 23:5]

The ideal Verilog would be:

module Bar(
  input        a,
  output [2:0] b
);
  wire  x = a + 1'h1; // @[main.scala 17:13]
  wire  y = x + 1'h1; // @[main.scala 18:13]
  assign b = {{2'd0}, y}; // @[main.scala 18:13]
endmodule

The Scala FIRRTL Compiler already generates something close to this. llvm/circt generates everything inlined.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.