Copy, clone and transaction ownership
Tell a second handle from a second object, write do_copy and do_compare for a transaction, and decide who may change an item after it is published.
- Lesson
- U2.1
- Time
- 25 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
A scoreboard keeps every item the monitor publishes in a queue and compares them with expected results at the end of the test. The design sends three results, 17, 128 and 0, and the scoreboard reports that all three entries hold x=255 and y=0. The design is fine; the monitor created one item object and refilled it for every observation, so the queue holds three handles to the same object.
Each queue entry is a handle to the monitor's one object, so every entry shows its latest contents.
A handle names an object
A class variable in SystemVerilog holds a handle, a reference to an object somewhere in memory. Assigning one class variable to another copies the handle. After b = a; there are two names and still one object, so a change made through a is visible through b.
An analysis port passes handles too. When the monitor calls ap.write(seen), every subscriber receives the same handle the monitor holds. Nothing is copied on the way. If the monitor later writes new values into seen, every subscriber that kept the handle sees them.
That is what happened in the opening story. The queue entries were stored at 10, 20 and 30 ns, but they were three handles to one object. At the end of the test they all read the last values written, x=255 and y=0.
Reusing one object looks efficient, which is why the bug is so common. It saves one allocation per observation, a small cost next to simulating the design itself. The price is that the object stops being a record of one observation and becomes a window onto the latest one.
There are two fixes, and both are common. The monitor can create a fresh item for every observation, which is what U1's monitor did. Or the subscriber can make its own copy before it keeps anything. The rest of this lesson is about making that copy correctly.
copy fills an object; clone makes one
Every class derived from uvm_object has two copying methods. a.copy(b) copies the contents of b into the existing object a. b.clone() creates a new object of the same type as b, copies b into it and returns it.
| Code | Objects afterwards | Later change to a.x |
|---|---|---|
b = a; | One object, two handles | Visible through b |
b = new("b"); b.copy(a); | Two objects | Not visible through b |
$cast(b, a.clone()); | Two objects | Not visible through b |
Assignment shares an object; copy and clone produce a second one.
clone() is declared to return a uvm_object, because it is defined once in the base class for every possible type. The object it returns really is an incr_item, but the compiler only knows the declared type. So you assign it with $cast, which checks the real type at run time and fails if it does not match.
The new object has the same type as the original, even when you clone through a base-class handle. clone() calls the object's own create() method, which the uvm_object_utils macro writes for each class. Cloning a derived item through a base handle therefore gives you a derived item, with all its extra fields. And because create() calls new directly, a factory override never changes the type of a clone.
Writing do_copy and do_compare
Registering a class with uvm_object_utils does not tell UVM which fields it has. copy() and clone() handle the bookkeeping, then call a hook named do_copy, which you write. Without one, a clone of incr_item has x and y at their default value, 0.
Call super.do_copy(rhs) first
The base class copies its own fields. In a class hierarchy, each level copies only what it declares.
Cast rhs to your type
The argument arrives as a
uvm_object. A failed cast means someone copied an unrelated object, so stop with a fatal error.Copy each field
One assignment per field. A field you forget keeps its old value in the copy, with no message at all.
do_compare follows the same pattern. You call a.compare(b), which returns 1 when they match, and it calls your do_compare to check the fields. A scoreboard uses it to compare an expected item with an observed one without listing the fields again.
Most teams add convert2string() as well, returning a one-line summary such as x=16 y=17. Report messages then print an item with one call, and a mismatch message shows both items in the same format.
Nested objects: share or clone
A transaction may contain handles to other objects, for example a packet with a payload object holding its data bytes. In do_copy, the line body = that.body; copies the handle. The copy and the original then share one payload, which is a shallow copy of that field.
A deep copy gives the copy its own payload: $cast(body, that.body.clone());. Now the producer can keep appending bytes to its payload without changing the copy.
Choose per field. Clone anything the producer might still change, such as a data buffer it reuses. Sharing is fine for something nobody changes during the test, such as a configuration object, and it saves memory in a long run.
Who may change an item
Bugs like the opening one come from two components changing, or keeping, the same object without agreeing on it. A short set of rules prevents them.
| Component | Rule |
|---|---|
| Monitor | Create a new item for every observation, and never touch it again after write() |
| Subscriber | Treat a received item as read-only; clone it before changing it, or before keeping it if the producer might reuse it |
| Sequence | Create a new item for each start_item; do not refill an item the driver may still hold |
| Driver | Change only the item it was given, and only fields meant for responses |
Who owns an item at each step of the bench.
The monitor rule is the cheapest. One type_id::create per observation costs almost nothing, and every subscriber can then keep the handle it receives. The subscriber rule is the defensive one: if you receive items from code you did not write, clone before you store.
Copies are not free, though. Cloning every item in a busy bench doubles the objects the simulator manages. Clone where an object is kept or changed, and not on every hop.
Try it
The monitor reuses one item, exactly as in the opening story. Two subscribers listen to it: keeper stores each handle it receives, and snapshot stores a clone. At the end, the keeper's three entries are one object showing the last values, while the snapshot holds three separate objects with the values seen at 10, 20 and 30 ns.
Delete the line y = that.y; from do_copy and run again: the snapshot's y values all become 0, and nothing warns you. Then move the create call in the monitor inside the foreach loop, and the keeper is correct too. The MISCMP message in the log comes from compare(), which reports the two objects that differ.
keeper: x=255 y=0 | x=255 y=0 | x=255 y=0 snapshot: x=16 y=17 | x=127 y=128 | x=255 y=0 keeper entries are one object: 1 snapshot[0] matches snapshot[2]: 0 UVM_ERROR : 0Check yourself
Remember
- Assigning a class variable copies a handle; only
copy()andclone()make a second object. - An analysis port passes the monitor's handle to every subscriber, so a reused item corrupts everything that kept it.
clone()returns auvm_objectof the original's real type; assign it with$cast.- Write
do_copyanddo_compare: callsuper, castrhs, then handle every field, choosing per nested object whether to share or clone it. - Monitors create a new item for every observation; subscribers clone before they change or keep an item from a producer that reuses objects.