Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid making unnecessary clones during binding #2611

Merged
merged 6 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/src/main/scala/chisel3/Aggregate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1302,8 +1302,18 @@ abstract class Bundle(implicit compileOptions: CompileOptions) extends Record {
)
}
}
}

private case class memoVal[T](var value: Option[T], val fn: () => T) {
def get(): T = {
if (value.isEmpty) {
value = Some(fn())
}
value.get
}
}
private var externalRef = memoVal[Boolean](None, () => elements.exists(_._2._id < _id))
private[chisel3] def hasExternalRef(): Boolean = externalRef.get()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private var externalRef = memoVal[Boolean](None, () => elements.exists(_._2._id < _id))
private[chisel3] def hasExternalRef(): Boolean = externalRef.get()
lazy val hasExternalRef: Boolean = this.elements.exists(_._2._id < _id)

Scala actually has a built-in for this sort of thing 🙂 This should also use less memory because it only adds a 1 byte field (and uses 1 extra bit in the lazy val mask that already exists because we have other lazy vals in Bundles). Your approach adds a 4-byte field pointing to a 24-40 byte object. This might actually suggest rerunning the memory use check because 28-bytes per Bundle is non-trivial.


override def cloneType: this.type = {
val clone = _cloneTypeImpl.asInstanceOf[this.type]
Expand Down
52 changes: 32 additions & 20 deletions core/src/main/scala/chisel3/Data.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,18 @@ object SpecifiedDirection {
}

private[chisel3] def specifiedDirection[T <: Data](
source: T
)(dir: SpecifiedDirection
source: => T
)(dir: T => SpecifiedDirection
)(
implicit compileOptions: CompileOptions
): T = {
val prevId = Builder.idGen.value
val data = source // evaluate source once (passed by name)
if (compileOptions.checkSynthesizable) {
requireIsChiselType(source)
requireIsChiselType(data)
}
val out = source.cloneType.asInstanceOf[T]
out.specifiedDirection = dir
val out = if (!data.mustClone(prevId)) data else data.cloneType.asInstanceOf[T]
out.specifiedDirection = dir(out)
out
}

Expand Down Expand Up @@ -427,19 +429,19 @@ object chiselTypeOf {
* Thus, an error will be thrown if these are used on bound Data
*/
object Input {
def apply[T <: Data](source: T)(implicit compileOptions: CompileOptions): T = {
SpecifiedDirection.specifiedDirection(source)(SpecifiedDirection.Input)
def apply[T <: Data](source: => T)(implicit compileOptions: CompileOptions): T = {
SpecifiedDirection.specifiedDirection(source)(_ => SpecifiedDirection.Input)
}
}
object Output {
def apply[T <: Data](source: T)(implicit compileOptions: CompileOptions): T = {
SpecifiedDirection.specifiedDirection(source)(SpecifiedDirection.Output)
def apply[T <: Data](source: => T)(implicit compileOptions: CompileOptions): T = {
SpecifiedDirection.specifiedDirection(source)(_ => SpecifiedDirection.Output)
}
}

object Flipped {
def apply[T <: Data](source: T)(implicit compileOptions: CompileOptions): T = {
SpecifiedDirection.specifiedDirection(source)(SpecifiedDirection.flip(source.specifiedDirection))
def apply[T <: Data](source: => T)(implicit compileOptions: CompileOptions): T = {
SpecifiedDirection.specifiedDirection(source)(x => SpecifiedDirection.flip(x.specifiedDirection))
}
}

Expand All @@ -462,6 +464,19 @@ abstract class Data extends HasId with NamedComponent with SourceInfoDoc {
}
}

// must clone a data if
// * it has a binding
// * its id is older than prevId (not "freshly created")
// * it is a bundle with a non-fresh member (external reference)
private[chisel3] def mustClone(prevId: Long): Boolean = {
if (this.hasBinding || this._id <= prevId) true
else
this match {
case b: Bundle => b.hasExternalRef()
case _ => false
}
}

override def autoSeed(name: String): this.type = {
topBindingOpt match {
// Ports are special in that the autoSeed will keep the first name, not the last name
Expand All @@ -475,13 +490,6 @@ abstract class Data extends HasId with NamedComponent with SourceInfoDoc {
private var _specifiedDirection: SpecifiedDirection = SpecifiedDirection.Unspecified
private[chisel3] def specifiedDirection: SpecifiedDirection = _specifiedDirection
private[chisel3] def specifiedDirection_=(direction: SpecifiedDirection) = {
if (_specifiedDirection != SpecifiedDirection.Unspecified) {
this match {
// Anything flies in compatibility mode
case t: Record if !t.compileOptions.dontAssumeDirectionality =>
case _ => throw RebindingException(s"Attempted reassignment of user-specified direction to $this")
}
}
jackkoenig marked this conversation as resolved.
Show resolved Hide resolved
_specifiedDirection = direction
}

Expand Down Expand Up @@ -511,6 +519,8 @@ abstract class Data extends HasId with NamedComponent with SourceInfoDoc {
_binding = Some(target)
}

private[chisel3] def hasBinding: Boolean = _binding.isDefined

// Similar to topBindingOpt except it explicitly excludes SampleElements which are bound but not
// hardware
private[chisel3] final def isSynthesizable: Boolean = _binding.map {
Expand Down Expand Up @@ -882,11 +892,13 @@ trait WireFactory {
/** Construct a [[Wire]] from a type template
* @param t The template from which to construct this wire
*/
def apply[T <: Data](t: T)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): T = {
def apply[T <: Data](source: => T)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): T = {
val prevId = Builder.idGen.value
val t = source // evaluate once (passed by name)
if (compileOptions.declaredTypeMustBeUnbound) {
requireIsChiselType(t, "wire type")
}
val x = t.cloneTypeFull
val x = if (!t.mustClone(prevId)) t else t.cloneTypeFull

// Bind each element of x to being a Wire
x.bind(WireBinding(Builder.forcedUserModule, Builder.currentWhen))
Expand Down
27 changes: 16 additions & 11 deletions core/src/main/scala/chisel3/Module.scala
Original file line number Diff line number Diff line change
Expand Up @@ -187,21 +187,26 @@ package experimental {
* requested (so that all calls to ports will return the same information).
* Internal API.
*/
def apply[T <: Data](iodef: T): T = {
def apply[T <: Data](iodef: => T): T = {
val module = Module.currentModule.get // Impossible to fail
require(!module.isClosed, "Can't add more ports after module close")
requireIsChiselType(iodef, "io type")
val prevId = Builder.idGen.value
val data = iodef // evaluate once (passed by name)
requireIsChiselType(data, "io type")

// Clone the IO so we preserve immutability of data types
// Note: we don't clone if the data is fresh (to avoid unnecessary clones)
val iodefClone =
try {
iodef.cloneTypeFull
} catch {
// For now this is going to be just a deprecation so we don't suddenly break everyone's code
case e: AutoClonetypeException =>
Builder.deprecated(e.getMessage, Some(s"${iodef.getClass}"))
iodef
}
if (!data.mustClone(prevId)) data
else
try {
data.cloneTypeFull
} catch {
// For now this is going to be just a deprecation so we don't suddenly break everyone's code
case e: AutoClonetypeException =>
Builder.deprecated(e.getMessage, Some(s"${data.getClass}"))
data
}
module.bindIoInPlace(iodefClone)
iodefClone
}
Expand Down Expand Up @@ -568,7 +573,7 @@ package experimental {
* TODO(twigg): Specifically walk the Data definition to call out which nodes
* are problematic.
*/
protected def IO[T <: Data](iodef: T): T = chisel3.experimental.IO.apply(iodef)
protected def IO[T <: Data](iodef: => T): T = chisel3.experimental.IO.apply(iodef)

//
// Internal Functions
Expand Down
7 changes: 4 additions & 3 deletions core/src/main/scala/chisel3/Reg.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,19 @@ object Reg {
* Value will not change unless the [[Reg]] is given a connection.
* @param t The template from which to construct this wire
*/
def apply[T <: Data](t: T)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): T = {
def apply[T <: Data](source: => T)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): T = {
val prevId = Builder.idGen.value
val t = source // evaluate once (passed by name)
if (compileOptions.declaredTypeMustBeUnbound) {
requireIsChiselType(t, "reg type")
}
val reg = t.cloneTypeFull
val reg = if (!t.mustClone(prevId)) t else t.cloneTypeFull
val clock = Node(Builder.forcedClock)

reg.bind(RegBinding(Builder.forcedUserModule, Builder.currentWhen))
pushCommand(DefReg(sourceInfo, reg, clock))
reg
}

}

/** Utility for constructing one-cycle delayed versions of signals
Expand Down
2 changes: 1 addition & 1 deletion src/test/scala/chiselTests/CompatibilitySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object CompatibilityCustomCompileOptions {
}
}

class CompatibiltySpec extends ChiselFlatSpec with ScalaCheckDrivenPropertyChecks with Utils {
class CompatibilitySpec extends ChiselFlatSpec with ScalaCheckDrivenPropertyChecks with Utils {
import Chisel._

behavior.of("Chisel compatibility layer")
Expand Down
32 changes: 32 additions & 0 deletions src/test/scala/chiselTests/LazyCloneSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0

package chiselTests

import chisel3._
import chisel3.stage.ChiselStage
import chiselTests.ChiselFlatSpec

class LazyCloneSpec extends ChiselFlatSpec {
behavior.of("LazyClone")

it should "lazily clone" in {
object Foo {
var count = 0L
}

class Foo extends Bundle {
val a = UInt(8.W)
Foo.count += 1
}

class MyModule extends RawModule {
val io = IO(Flipped(new Bundle {
val foo = Output(new Foo)
val bar = Input(new Foo)
}))
}
ChiselStage.elaborate(new MyModule)
println(s"copies: ${Foo.count}")
Foo.count should be < 12L
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
println(s"copies: ${Foo.count}")
Foo.count should be < 12L
Foo.count should be(2)

Also, it would be good to have a test that sharing a single Foo for the Input and Output does result in cloning.

class MyModule extends RawModule {
  val foo = new Foo
   val io = IO(Flipped(new Bundle {
     val foo = Output(foo)
     val bar = Input(foo)
   }))
 }
// This should have 3 Foos constructed
object Bar {
   var count = 0L
 }

 class Bar(x: UInt) extends Bundle {
   val a = x
   Bar += 1
 }
class MyModule extends RawModule {
   val io = IO(Flipped(new Bundle {
     val foo = Output(new Bar(UInt(8.W)))
     val bar = Input(new Bar(UInt(8.W)))
   }))
 }
// This should have 4 Bars constructed

Also a test that shows an extra clone when you pass a Data object to the Bundle that is used as a field (aka an externalRef).

}
}