Object Cloning
clone() vs copy(), the handle aliasing bug, transaction pipeline cloning, $cast pattern.
UVM Fundamentals · Module 18
§1 — The Bug That Passes Tests It Shouldn't
Picture this: your AXI scoreboard is showing 100% pass rate across 5,000 transactions. Your manager is happy. Tapeout is three weeks out. Then a DV lead on another team spots something odd — the scoreboard's expected queue is being populated correctly, but when a comparison happens, the expected and actual transactions look identical even when you deliberately inject an error into the DUT.
The culprit is almost always the same: a monitor that creates one transaction object, reuses it in a loop, and passes the same handle to every subscriber via the analysis port. By the time the scoreboard tries to compare, it's comparing the transaction against a later version of itself — because both the "expected" entry in the queue and the "actual" observation point to the same piece of simulation memory.
This is the handle aliasing problem. It's invisible in simulation logs, it produces no errors, and it causes your testbench to silently miss real RTL bugs. The fix is clone() — but understanding when to clone, when to use copy() instead, and what "deep" vs "shallow" really means in the context of your transaction hierarchy is what separates engineers who debug this in five minutes from those who spend three days on it.
§2 — clone() vs copy() — Build the Intuition First
Before the syntax, understand what each operation actually does to simulation memory. In SystemVerilog, a class variable is a handle — not the object itself. Think of it like a pointer in C, or a reference in Python. The handle is a 4-byte value that stores a memory address. The object is the actual data living at that address.
When you assign one handle to another — txn_b = txn_a — you get two handles pointing at the same object. Change any field through either handle and both handles see the change. That's aliasing.
| Operation | Allocates new memory? | Copies field values? | Result |
|---|---|---|---|
txn_b = txn_a | NO | NO | Two handles point to the same object — aliased. Change through one, both see it. |
txn_b.copy(txn_a) | NO | YES | Fields copied from txn_a into existing txn_b. txn_b must already exist. Changes to txn_a after this do NOT affect txn_b. |
$cast(txn_b, txn_a.clone()) | YES | YES | New object allocated, all fields copied. txn_b is completely independent. This is what monitor pipelines need. |
The key distinction between copy() and clone() is memory allocation. copy() updates an existing object — you provide the destination. clone() creates a new object for you and returns it. In a monitor's run loop, you almost always want clone() because you need a fresh object for each observation that subscribers can store independently.
§3 — Syntax and the $cast Pattern
clone() returns a uvm_object handle — not your specific class type. That's why you always need $cast() immediately after. This trips up almost everyone the first time.
// ── clone(): allocate + copy ────────────────────────────────────────
// Returns uvm_object — MUST $cast to your type
apb_txn original, cloned;
original = apb_txn::type_id::create("orig");
void'(original.randomize());
// Pattern 1: $cast on a separate line (most readable)
begin
uvm_object tmp;
tmp = original.clone(); // returns uvm_object
if (!$cast(cloned, tmp)) // downcast to apb_txn
`uvm_fatal("CLONE", "Cast failed")
end
// Pattern 2: Inline $cast (most common in production code)
$cast(cloned, original.clone());
// Pattern 3: With null check (most defensive)
if (!$cast(cloned, original.clone()))
`uvm_fatal("CLONE", "clone() returned wrong type")
// ── copy(): no allocation — destination must already exist ──────────
apb_txn dst = apb_txn::type_id::create("dst");
dst.copy(original); // fields from original → dst; no new object created
// ── What clone() does internally (simplified) ───────────────────────
// function uvm_object uvm_object::clone();
// uvm_object copy = this.create(this.get_name()); // factory-based allocation
// if (copy == null) begin
// uvm_fatal("CLONE", "create() returned null")
// end
// copy.copy(this); // calls do_copy() on the new object
// return copy;
// endfunction
// ── Cloning and modifying — they are truly independent ───────────────
cloned.addr = 32'hFFFF_0000; // does NOT affect original.addr
original.data = 32'h1234_5678; // does NOT affect cloned.data§4 — Memory Diagrams — Seeing Handle Aliasing vs Independence
❌ WITHOUT clone() — HANDLE ALIASINGSINGLE OBJECT @0x4A00addr = 0x A000_0000data = 0x 1234_5678write = 1← Iteration N+1 overwrites these!scb.exp[0]cov.ap_implogger.bufAll three handle the SAME memoryWhen monitor rewrites fields next iteration,all three subscribers see the NEW values!✓ WITH clone() — INDEPENDENT OBJECTS@0x4A00 (orig)addr=0xA0..00data=0x12..@0x5B00 (clone1)addr=0xA0..00data=0x12..@0x6C00 (clone2)addr=0xA0..00data=0x12..scb.exp[0]cov.ap_implogger.bufEach subscriber owns its own copyMonitor can reuse its handle or bereallocated — zero impact on subscribersScoreboard compares expected @0x4A00 against actual @0x4A00 → always matches!Scoreboard correctly compares immutable snapshots → real bugs caught Figure 1 — Handle aliasing (left) vs clone() independence (right). Without cloning, all subscribers hold the same handle — the monitor's next write corrupts all of them simultaneously. With cloning, each subscriber holds its own independent snapshot.
Step-by-Step: What Happens in Each Iteration
| Iteration | Monitor action | Without clone — scoreboard sees | With clone — scoreboard sees |
|---|---|---|---|
| N=1 | Writes addr=0xA000, data=0x1234, calls write(txn) | exp[0] → addr=0xA000, data=0x1234 ✓ | exp[0] → addr=0xA000, data=0x1234 ✓ (own copy) |
| N=2 | Overwrites same txn: addr=0xB000, data=0x5678, calls write(txn) | exp[0] → addr=0xB000, data=0x5678 ❌ (corrupted!) | exp[0] → addr=0xA000, data=0x1234 ✓ (unchanged) |
| Compare | Scoreboard compares exp[0] to actual txn_0 | Compares {0xB000, 0x5678} to actual — wrong expected value! | Compares {0xA000, 0x1234} to actual — correct! |
§5 — Code Examples — From The Bug to the Fix
Example 1 — The Aliasing Bug in a Monitor
// ── ❌ WRONG: Single handle reused every iteration ────────────────────
task run_phase(uvm_phase phase);
apb_txn txn = apb_txn::type_id::create("txn"); // created ONCE outside loop
forever begin
@(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));
txn.addr = vif.paddr; // ← overwrites previous values
txn.data = vif.prdata;
txn.write = vif.pwrite;
analysis_port.write(txn); // all subscribers get the SAME handle
// Next iteration overwrites txn.addr/data/write
// → every stored handle now points to the new values
end
endtask
// ── ✓ PATTERN A: New object each iteration (cleanest) ─────────────────
task run_phase(uvm_phase phase);
forever begin
apb_txn txn; // declared inside loop — new null handle each iteration
@(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));
txn = apb_txn::type_id::create("txn"); // new object each iteration
txn.addr = vif.paddr;
txn.data = vif.prdata;
txn.write = vif.pwrite;
analysis_port.write(txn); // each subscriber gets its own unique object
end
endtask
// ── ✓ PATTERN B: Clone before write (useful when construction is expensive) ──
task run_phase(uvm_phase phase);
apb_txn txn = apb_txn::type_id::create("txn");
forever begin
apb_txn snap;
@(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));
txn.addr = vif.paddr;
txn.data = vif.prdata;
txn.write = vif.pwrite;
$cast(snap, txn.clone()); // snapshot before potentially overwriting
analysis_port.write(snap); // safe — snap is independent
end
endtask
// ── ✓ PATTERN C: copy() into pre-allocated buffer (high-throughput systems) ──
task run_phase(uvm_phase phase);
apb_txn scratch = apb_txn::type_id::create("scratch");
apb_txn published = apb_txn::type_id::create("published");
forever begin
@(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));
scratch.addr = vif.paddr;
scratch.data = vif.prdata;
scratch.write = vif.pwrite;
published.copy(scratch); // copy fields into pre-allocated 'published'
analysis_port.write(published);
// NOTE: This still has aliasing if subscribers store 'published' handle!
// Only safe if subscribers immediately consume and discard the write() argument
// Prefer Pattern A or B for storage-safe usage
end
endtaskExample 2 — Scoreboard Storing Clones for Later Comparison
class apb_scoreboard extends uvm_scoreboard;
`uvm_component_utils(apb_scoreboard)
`uvm_analysis_imp_decl(`REQ)
`uvm_analysis_imp_decl(`RSP)
uvm_analysis_imp_REQ#(apb_txn,apb_scoreboard) req_imp;
uvm_analysis_imp_RSP#(apb_txn,apb_scoreboard) rsp_imp;
apb_txn expected_q[$]; // stores CLONES — not the original handles
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
req_imp = new("req_imp", this);
rsp_imp = new("rsp_imp", this);
endfunction
function void write_REQ(apb_txn txn);
apb_txn stored_copy;
// CRITICAL: clone() before pushing — we don't own txn
// The monitor (or whoever called write()) still controls that handle
$cast(stored_copy, txn.clone());
stored_copy.set_name($sformatf("exp_%0d", expected_q.size()));
expected_q.push_back(stored_copy);
`uvm_info("SCB",$sformatf("Stored clone of txn, queue depth=%0d",
expected_q.size()),UVM_HIGH)
endfunction
function void write_RSP(apb_txn actual);
apb_txn expected;
if (expected_q.size() == 0) begin
`uvm_error("SCB","Response received but expected queue is empty")
return;
end
expected = expected_q.pop_front();
if (!expected.compare(actual))
`uvm_error("SCB",$sformatf(
"MISMATCH!\nExp: %s\nAct: %s",
expected.sprint(), actual.sprint()))
else
`uvm_info("SCB","Match ✓",UVM_HIGH)
endfunction
endclassExample 3 — Reference Model Cloning in Complex Pipelines
class axi_ref_model extends uvm_component;
`uvm_component_utils(axi_ref_model)
uvm_blocking_get_port#(axi_txn) stim_port;
uvm_analysis_port#(axi_txn) predicted_ap;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
stim_port = new("stim_port", this);
predicted_ap = new("predicted_ap", this);
endfunction
task run_phase(uvm_phase phase);
axi_txn stim, predicted;
forever begin
stim_port.get(stim);
// Clone before mutating — we don't own 'stim'
// The driver or sequence may still reference this object
$cast(predicted, stim.clone());
// Apply DUT transformations to the clone — never touch stim
if (predicted.write) begin
predicted.resp = 2'b00; // OKAY response for writes
end else begin
predicted.rdata = mem_model(predicted.addr);
predicted.resp = 2'b00;
end
// Publish the predicted response — scoreboard compares this to DUT output
predicted_ap.write(predicted);
end
endtask
function bit[31:0] mem_model(bit[31:0] addr);
// simplified memory model lookup
return mem[addr];
endfunction
bit[31:0] mem[bit[31:0]]; // associative array backing store
endclass§6 — Simulation Thinking — What the Simulator Actually Does
How clone() Resolves at Runtime
| Step | What Happens | Notes |
|---|---|---|
| 1 | txn.clone() is called | This is uvm_object::clone() — calls this.create(this.get_name()) via factory |
| 2 | Factory creates a new object of the same type | Respects type overrides — if you've overridden apb_txn, the clone is also the override type |
| 3 | New object calls new_obj.copy(this) | Which calls new_obj.do_copy(this) — your override or macro-generated implementation |
| 4 | Returns the new object as uvm_object | You must $cast() to your specific type |
| 5 | Original object is untouched | No delta cycles, no event triggers — purely functional |
// ── Verify clone() produces independent objects ───────────────────────
apb_txn a, b;
a = apb_txn::type_id::create("a");
void'(a.randomize());
automatic bit[31:0] orig_addr = a.addr;
$cast(b, a.clone());
// Verify independence: modify a, b should not change
a.addr = 32'hFFFF_FFFF;
`uvm_info("T",$sformatf(
"a.addr=0x%0h b.addr=0x%0h same?=%0b",
a.addr, b.addr, (a.addr === b.addr)),UVM_NONE)
// Expected: a.addr=0xFFFFFFFF b.addr=0x???????? same?=0 ← independent!
// ── Verify clone() respects factory override ──────────────────────────
// If test registered: apb_txn → debug_apb_txn
// Then: type_name of the clone should be debug_apb_txn
`uvm_info("T",$sformatf("Clone type: %s", b.get_type_name()),UVM_NONE)
// Without override: "apb_txn"
// With override: "debug_apb_txn" ← factory works through clone()§7 — Real Verification Usage — Where Cloning Saves Projects
| Component | Cloning Pattern | Why |
|---|---|---|
| Monitor | Create new object per iteration OR clone before write() | Each subscriber gets an immutable snapshot — scoreboard, coverage, and logger see consistent data |
| Scoreboard write_REQ() | Clone before push to expected queue | The write() argument is borrowed — the caller may reuse it. Store your own copy. |
| Reference Model | Clone stimulus before mutating | Stimulus object may be referenced by the sequence or driver. Mutation without clone corrupts their view. |
| Coverage Collector | Often safe without clone — depends on usage | If you sample immediately in write() and don't store the handle, no clone needed. If you defer, clone. |
| Golden Queue | Always clone before storing | The golden reference must never alias with live objects in the testbench |
| Sequence Callback | Clone if modifying the transaction | The sequence item belongs to the sequencer-driver handshake — modification without clone corrupts the pipeline |
§8 — Bugs and Debugging — Real Scenarios
Bug 1 — Scoreboard Shows 100% Pass on a Broken DUT
Symptom: You inject a known data corruption into the DUT. The scoreboard doesn't report any mismatch. You know the DUT is outputting wrong data because you can see it in the waveform. But compare() returns 1 every single time.
Root Cause: Monitor reuses the same transaction handle. By the time the scoreboard compares, the expected and actual handles point to the same memory address — the most recent monitor observation. Comparing an object against itself always returns 1.
Diagnostic: Print $sformatf("%p", expected) and $sformatf("%p", actual) in the scoreboard. If the memory addresses match — you've found the aliasing.
// ── Diagnostic: detect aliasing with address comparison ───────────────
function void write_RSP(apb_txn actual);
apb_txn expected = expected_q.pop_front();
// Quick aliasing diagnostic — if addresses are same, we have a bug
`uvm_info("SCB_DBG",$sformatf(
"exp handle=%p act handle=%p same=%0b",
expected, actual, (expected === actual)), // === on handles = pointer compare
UVM_DEBUG)
// If same=1 → ALIASING BUG confirmed
if (!expected.compare(actual))
`uvm_error("SCB","Mismatch")
endfunction
// ── Bug 2: copy() without a pre-created destination ───────────────────
// ❌ WRONG: null handle — crash at txn_b.copy()
apb_txn txn_b; // null handle — no object yet
txn_b.copy(txn_a); // CRASH: null dereference
// ✓ CORRECT: create first, then copy
apb_txn txn_b = apb_txn::type_id::create("txn_b");
txn_b.copy(txn_a); // safe — txn_b already exists
// OR use clone() which handles allocation automatically:
$cast(txn_b, txn_a.clone()); // safer, simpler
// ── Bug 3: Forgetting to clone in write() — false coverage closure ────
// ❌ WRONG: coverage stores aliased handle
function void write(apb_txn txn);
cov_queue.push_back(txn); // ALIASED — later monitor writes corrupt history
endfunction
// ✓ CORRECT: clone before storing
function void write(apb_txn txn);
apb_txn snap;
$cast(snap, txn.clone());
cov_queue.push_back(snap);
endfunction§9 — Ready-to-Run Demo
Ready to Run — Questa / VCS / Xcelium
// clone_demo.sv — demonstrates handle aliasing bug and clone() fix
// Questa : vlog -sv clone_demo.sv && vsim -c clone_demo_top -do "run -all; quit"
// VCS : vcs -sverilog -ntb_opts uvm clone_demo.sv && ./simv
// Xcelium: xrun -sv -uvm clone_demo.sv -input "run; exit"
`include "uvm_macros.svh"
import uvm_pkg::*;
class pkt extends uvm_sequence_item;
`uvm_object_utils(pkt)
rand bit[7:0] id;
rand bit[31:0] data;
function new(string n="pkt"); super.new(n); endfunction
function void do_copy(uvm_object rhs);
pkt r; super.do_copy(rhs); $cast(r,rhs);
id=r.id; data=r.data;
endfunction
endclass
class clone_test extends uvm_test;
`uvm_component_utils(clone_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
pkt p, q;
phase.raise_objection(this);
`uvm_info("TEST","═══ DEMONSTRATION 1: Handle aliasing bug ═══",UVM_NONE)
p = pkt::type_id::create("p");
p.id=1; p.data=32'hAAAA_BBBB;
q = p; // handle assignment — ALIASING
`uvm_info("TEST",$sformatf("Before: p.data=0x%0h q.data=0x%0h",
p.data,q.data),UVM_NONE)
p.data = 32'hCCCC_DDDD; // modify p
`uvm_info("TEST",$sformatf("After p.data change: p.data=0x%0h q.data=0x%0h (BOTH CHANGED!)",
p.data,q.data),UVM_NONE)
`uvm_info("TEST","═══ DEMONSTRATION 2: clone() independence ═══",UVM_NONE)
p.data = 32'hAAAA_BBBB;
$cast(q, p.clone()); // deep, independent copy
`uvm_info("TEST",$sformatf("Before: p.data=0x%0h q.data=0x%0h",
p.data,q.data),UVM_NONE)
p.data = 32'hCCCC_DDDD; // modify p
`uvm_info("TEST",$sformatf("After p.data change: p.data=0x%0h q.data=0x%0h (q UNCHANGED!)",
p.data,q.data),UVM_NONE)
`uvm_info("TEST","═══ DEMONSTRATION 3: copy() requires existing destination ═══",UVM_NONE)
p.data = 32'h1234_5678;
q = pkt::type_id::create("q"); // must exist before copy()
q.copy(p);
`uvm_info("TEST",$sformatf("After copy: p.data=0x%0h q.data=0x%0h (same? %0b)",
p.data,q.data,(p.data===q.data)),UVM_NONE)
p.data = 32'hFFFF_0000;
`uvm_info("TEST",$sformatf("After p change: p.data=0x%0h q.data=0x%0h (q independent? %0b)",
p.data,q.data,(p.data!==q.data)),UVM_NONE)
`uvm_info("TEST","═══ DEMONSTRATION 4: Monitor loop aliasing vs fix ═══",UVM_NONE)
begin
pkt queue[$];
pkt monitor_txn = pkt::type_id::create("m");
// BUGGY: reuse same handle
for(int i=0;i<3;i++) begin
monitor_txn.id=i; monitor_txn.data=i*100;
queue.push_back(monitor_txn); // all aliases!
end
`uvm_info("TEST",$sformatf(
"Buggy queue: [0].id=%0d [1].id=%0d [2].id=%0d (all same!)",
queue[0].id,queue[1].id,queue[2].id),UVM_NONE)
queue.delete();
// FIXED: clone each iteration
for(int i=0;i<3;i++) begin
pkt snap;
monitor_txn.id=i; monitor_txn.data=i*100;
$cast(snap, monitor_txn.clone());
queue.push_back(snap); // each is independent
end
`uvm_info("TEST",$sformatf(
"Fixed queue: [0].id=%0d [1].id=%0d [2].id=%0d (each correct!)",
queue[0].id,queue[1].id,queue[2].id),UVM_NONE)
end
phase.drop_objection(this);
endtask
endclass
module clone_demo_top;
initial run_test("clone_test");
endmodule
// EXPECTED OUTPUT:
// TEST: ═══ DEMONSTRATION 1: Handle aliasing bug ═══
// TEST: Before: p.data=0xaaaabbbb q.data=0xaaaabbbb
// TEST: After p.data change: p.data=0xccccdddd q.data=0xccccdddd (BOTH CHANGED!)
// TEST: ═══ DEMONSTRATION 2: clone() independence ═══
// TEST: Before: p.data=0xaaaabbbb q.data=0xaaaabbbb
// TEST: After p.data change: p.data=0xccccdddd q.data=0xaaaabbbb (q UNCHANGED!)
// TEST: ═══ DEMONSTRATION 3: copy() requires existing destination ═══
// TEST: After copy: p.data=0x12345678 q.data=0x12345678 (same? 1)
// TEST: After p change: p.data=0xffff0000 q.data=0x12345678 (q independent? 1)
// TEST: ═══ DEMONSTRATION 4: Monitor loop aliasing vs fix ═══
// TEST: Buggy queue: [0].id=2 [1].id=2 [2].id=2 (all same!)
// TEST: Fixed queue: [0].id=0 [1].id=1 [2].id=2 (each correct!)§10 — Interview Questions
- What is the difference between clone() and copy() in UVM?
clone()allocates a new object and copies all fields into it — it is allocation + copy.copy(rhs)copies field values fromrhsinto an already-existing object — no allocation occurs. Useclone()when you need a fresh independent copy. Usecopy()when you have an existing object you want to update with another's values. - Why does a monitor need to clone transactions before calling analysis_port.write()? The analysis port passes the same handle to every subscriber. If the monitor reuses the same transaction object across iterations, all previously stored handles (in scoreboard queues, coverage collectors, etc.) point to the same memory and see the latest iteration's values — not the values at the time they were "published." This handle aliasing causes scoreboards to compare incorrect expected values. Cloning gives each subscriber an independent snapshot that cannot be corrupted by future monitor iterations.
- clone() returns uvm_object. Why is $cast() always needed and what happens if you skip it?
clone()is defined inuvm_objectwith return typeuvm_object. Your specific class type information is lost at the return point. Without$cast(), you can only access methods defined onuvm_object— not your custom fields likeaddr,data, etc. If you skip$cast()and directly assign, the compiler rejects it with a type mismatch. If you usevoid'(clone())and discard the result, you've cloned for nothing — the new object is immediately garbage collected with no handles pointing to it. - Tricky: You override apb_txn with debug_apb_txn in your test. Does a call to apb_txn.clone() return an apb_txn or debug_apb_txn? It returns
debug_apb_txn.clone()internally callsthis.create(this.get_name()), which goes through the UVM factory. Since the factory hasapb_txn → debug_apb_txnregistered, the factory returns adebug_apb_txn. This is one of the most powerful — and overlooked — benefits of clone(): it propagates factory overrides through all cloning operations, so your debug transaction type automatically flows through monitors and reference models without changing their code. - When is it safe to NOT clone in a write() callback? It is safe to skip cloning in
write()only when: (1) you do not store the handle anywhere — you process the transaction and discard it completely within the same function call, AND (2) you do not call any methods that consume simulation time (since write() is a function, this is typically guaranteed). If you only sample fields, increment counters, or sample coverage — no handle storage, no time consumption — cloning is unnecessary overhead. The moment youpush_back(txn)to any queue or store it to a class variable, you must clone first.
§11 — Best Practices and Engineering Summary
| Rule | Why It Matters |
|---|---|
| In a monitor loop: always create a new object per iteration or clone before write() | This is the #1 rule. Every other rule flows from this one. Handle aliasing in monitors silently disables your scoreboard's ability to catch real RTL bugs. |
| In write() callbacks: clone before push_back() to any queue | You don't own the write() argument. The caller may reuse or modify it after returning from your write() function. |
| Use $cast() with a null check for safety | If do_copy() has a bug and returns the wrong type, $cast() will fail. Catching it with uvm_fatal gives you a meaningful error rather than a null-pointer crash later. |
| Before mutating a borrowed object, clone it first | In reference models and callbacks, if you receive an object you don't own, clone before modifying. Otherwise you corrupt the original. |
| In do_copy(), deep-copy all nested uvm_object handles | clone() only goes as deep as your do_copy() implementation. Shallow copy of nested objects means aliasing at the next level down. |
| Trust $cast() in test code, use type checking in library code | In reusable VIP code, add the null check. In your own test code where you control both sides, inline $cast is fine. |
| Write a simple aliasing diagnostic test | Compare handles with ===. If expected handle === actual handle, you've got aliasing. Add this check to your scoreboard during development and remove it before tapeout. |
Handle aliasing is a category of bug that hides in plain sight. The code compiles. The simulation runs. The scoreboard reports PASS. And your DUT is shipping broken RTL because the testbench was comparing a transaction to a copy of itself.
clone() is a seven-character fix that prevents that scenario entirely. The engineers who get burned by aliasing once never write a monitor without it again. The engineers who learn it from this module never get burned at all.