Shallow copies and deep copies
Choose between a handle copy, a shallow copy and a deep copy, and prove that a copy no longer shares anything it should own.
- Lesson
- 4.2
- Time
- 20 min
- Runs on
- Verilator
- Check
- 5 questions
A monitor publishes each read to the scoreboard, then reuses the same object for the next burst. The scoreboard protects itself with a shallow copy, exp = new tr;, but the data beats live in a nested payload object. The copy gets its own ID and address yet shares the monitor's payload, so every stored expectation shows the beats of the latest burst. Old reads start failing as soon as new traffic arrives.
The copy kept its own ID, but its payload is the monitor's payload.
Three things called a copy
A handle copy, b = a;, creates no object at all. Both handles refer to the original, as the previous lesson showed.
A shallow copy, s = new a;, creates a new object and copies every property of a into it. Properties that are themselves handles are copied as handles, so both objects then refer to the same nested objects.
A deep copy creates a new object and new copies of the nested objects it owns. SystemVerilog has no built-in deep copy, so you write a method for it, usually called copy or clone.
What each copy shares
mon = new(3);
The constructor also builds a payload object for the beats.
mon txn 1 (id 3)txn 1 body payload P1h = mon;
A handle copy: no new object.
h txn 1mon txn 1s = new mon;
A shallow copy: a new transaction, but its body handle points at the same payload.
s txn 2 (id 3)txn 2 body payload P1d = mon.copy();
A deep copy: a new transaction with a new payload holding the same beats.
d txn 3 (id 3)txn 3 body payload P2
Now edit the monitor's beats. The handle copy and the shallow copy both see the change, because both reach payload P1. Only the deep copy keeps the old beats.
What a shallow copy really copies
A shallow copy copies every value-type property: integers, enums, packed structs, strings, and arrays and queues of those. A queue property such as int beats[$] is copied element by element, so the copy gets its own queue.
Only class handles are shared. That is why the trouble in the opening story came from the nested payload object, and not from the ID or the address.
A shallow copy also skips the constructor. The new object is filled straight from the old one. Any work the constructor does, such as building a fresh payload or taking a new ID from a counter, does not happen.
Writing a deep copy
A copy method builds a new object with new, copies the value properties, and then builds a new nested object for every handle the transaction owns. A nested class can provide its own copy method, so each level copies itself.
Shallow copy
exp = new tr;
// exp.body == tr.body:
// one payload, two owners
The stored expectation changes whenever the monitor refills its payload.
Deep copy method
function axi_txn copy();
axi_txn c = new(id, addr); // builds c.body
c.body.beats = body.beats; // copies the values
return c;
endfunction
...
exp = tr.copy();
The expectation owns its own payload, so later bursts cannot change it.
Handle a null nested handle on purpose. If a transaction may have no payload, the copy should leave the copy's payload null as well, instead of dereferencing a null handle.
When sharing is right
Not every nested object should be copied. A configuration object that no one changes during the test can be shared by thousands of transactions, which saves memory and keeps them consistent.
Decide for each handle property whether the transaction owns it or merely refers to it, and write that decision as a comment next to the property. The copy method then follows the comments.
A copy needs a matching compare
Once a scoreboard stores copies, it also needs to compare them with what the DUT returns. == on handles is no help, because a copy is always a different object.
Write a compare(axi_txn other) method that checks each property that matters, and descends into nested objects just as copy does. If copy copies the payload beats, compare must compare them too.
Keep the two methods side by side in the class. When someone adds a property, both need updating, and seeing them together makes a forgotten field easier to spot.
Testing a copy
A copy test changes every level of the copy and then checks that the original did not move. Checking that the top-level handles differ is not enough, because a shallow copy passes that test.
Check the nested handles too: after a deep copy, d.body != mon.body must hold. Run the same test with a null payload if the class allows one.
Try it
The handle copy follows every change. The shallow copy kept its own ID, 3, but shows the new beats through the shared payload. Only the deep copy still holds the first burst.
Delete the line c.body.beats = body.beats; from copy and predict the deep copy's line. Then think about why that output is also wrong.
handle copy: id=4 beats=77,88 shallow copy: id=3 beats=77,88 deep copy: id=3 beats=11,22 shares the monitor's payload: shallow=1 deep=0Check yourself
Remember
- A handle copy creates no object; a shallow copy
new acreates one but shares nested objects; a deep copy shares nothing it owns. - A shallow copy copies values, including arrays and queues, but copies class handles as handles.
- A shallow copy skips the constructor.
- Write a
copymethod that builds new nested objects for everything the transaction owns. - Test a copy by changing a nested field of the copy and checking the original.
Practice
- The copy that shares its payload Fix · Easy
- Transactions that expose broken compares Break it · Medium