Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
    • What Is UVM?
      • Why UVM Exists: History and Goals
      • What UVM Standardizes (and What You Own)
      • Components vs Objects: Hierarchy, Lifetime, and Phases
      • Objects That Copy, Compare and Print
      • The Stimulus, Check, and Coverage Triangle
      • Your First UVM Testbench
      • The DMA Bench: Full Source, Part 1
      • The DMA Bench: Full Source, Part 2
      • The DMA Bench: Full Source, Part 3
    • Testbench Architecture and Environment Composition
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹What Is UVM?
  1. Topics
  2. UVM Verification
  3. UVM Foundations
  4. What Is UVM?
  5. Objects That Copy, Compare and Print

Part 1 · Foundations · Intermediate

Objects That Copy, Compare and Print

Field macros versus hand-written do_copy, do_compare and do_print: comparer and printer policies, what clone() really does, and why the DMA bench clones.

On this page

    A scoreboard reports item mismatch on one transaction in ten thousand, and only on long regressions. You add a print of both items and they look identical. After a day you find it: the item's compare ignores a field the designer cares about, the copy the scoreboard stored is shallow, and the printed values were read after the monitor had changed the shared object underneath. Three methods every item has (copy, compare, print), and each one hid part of the bug. How should they be written, and who decides what they do?

    The idea: a class should know how to duplicate and describe itself

    Every uvm_object has a family of utility methods: copy, clone, compare, print, sprint, convert2string, plus pack and record. The public method is fixed by UVM; the class-specific work happens in a protected hook with a do_ prefix (do_copy, do_compare, do_print). You fill those hooks in one of two ways. Field macros (uvm_field_int and friends, inside uvm_object_utils_begin/end) generate the work from a list of fields. Hand-written do_* methods spell it out.

    Think of it like a form letter versus a handwritten note. The form letter (macros) is quick to produce, consistent and easy to forget about, and that last point is the problem: it does exactly what it was set up to do, which may not be what you meant. The handwritten note costs more effort but says precisely what you intend, and anyone reading the code sees it.

    The picture: who does the work in each call

    You call

    UVM does

    Your class supplies

    a.copy(b)

    Checks for null, sets up a copier policy, calls the hook

    do_copy(rhs): copy each field from rhs

    a.clone()

    create(get_name()) on the same class, then copy(this)

    Nothing extra: it reuses do_copy

    a.compare(b)

    Sets up a comparer policy, calls the hook, reports miscompares

    do_compare(rhs, comparer): return 1 if equal

    a.print() / a.sprint()

    Sets up a printer policy (table, line, tree), formats the output

    do_print(printer): name each field

    a.convert2string()

    Nothing: the default returns an empty string

    A one-line summary for uvm_info messages

    Two details in that table carry interview weight. `clone()` is not a factory call. It calls the object's own create() method, which the registration macro defines as a plain new of the same class, so factory overrides are not consulted and the clone has the same dynamic type as the original. And the comparer and printer are policy objects: they decide formatting, how many miscompares to report, how deep to recurse into nested objects, and your do_* methods talk to them rather than printing or reporting directly.

    Build it: the DMA's stream item, by hand

    The bench's items already hand-write do_copy and convert2string, because the truncating driver relies on clone(). Here is axis_item with the other two methods added, the way you would extend it for a scoreboard that compares whole items.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // snippet: axis_item with all four utilities hand-written
    class axis_item extends uvm_sequence_item;
      rand bit [31:0]   data;
      rand bit          last;
      rand int unsigned idle;          // driver timing knob: never observed on the bus
      `uvm_object_utils(axis_item)
      function new(string name = "axis_item");
        super.new(name);
      endfunction
    
      function void do_copy(uvm_object rhs);
        axis_item r;
        super.do_copy(rhs);              // copy the parent class's fields first
        $cast(r, rhs);
        data = r.data; last = r.last; idle = r.idle;
      endfunction
    
      function bit do_compare(uvm_object rhs, uvm_comparer comparer);
        axis_item r;
        if (!$cast(r, rhs)) return 0;
        do_compare = super.do_compare(rhs, comparer);
        do_compare &= comparer.compare_field_int("data", data, r.data, 32, UVM_HEX);
        do_compare &= comparer.compare_field_int("last", last, r.last, 1);
        // idle is deliberately not compared: a monitored item never has it
      endfunction
    
      function void do_print(uvm_printer printer);
        super.do_print(printer);
        printer.print_field_int("data", data, 32, UVM_HEX);
        printer.print_field_int("last", last, 1, UVM_BIN);
        printer.print_field_int("idle", idle, 32, UVM_DEC);
      endfunction
    
      function string convert2string();
        return $sformatf("data=%08h%s", data, last ? " LAST" : "");
      endfunction
    endclass
    • `super.do_copy` and `super.do_compare` first, so a derived item (say, an error-injecting item with an extra field) can extend these methods without repeating its parent's fields.

    • `comparer.compare_field_int` instead of ==: it returns the same answer but also records which field differed, which is what you want in a failing regression's log.

    • `idle` is left out of the compare on purpose. It is a stimulus knob; the monitor never sees it, so comparing it would make every driver-side item mismatch every monitored one.

    The same item with field macros

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // snippet
    class axis_item_fm extends uvm_sequence_item;
      rand bit [31:0]   data;
      rand bit          last;
      rand int unsigned idle;
      `uvm_object_utils_begin(axis_item_fm)
        `uvm_field_int(data, UVM_ALL_ON | UVM_HEX)
        `uvm_field_int(last, UVM_ALL_ON)
        `uvm_field_int(idle, UVM_ALL_ON | UVM_NOCOMPARE | UVM_DEC)
      `uvm_object_utils_end
      function new(string name = "axis_item_fm");
        super.new(name);
      endfunction
    endclass

    Six lines instead of thirty, and the flags express the same intent: UVM_NOCOMPARE keeps idle out of the compare. The flags and the macro variants for arrays, objects and enums are covered in Field macros. The question is not which is shorter but which you would rather debug at 2 am.

    Watch it run

    Both versions were run through the same script with the course's Verilator flow: clone an item, change one field, compare, print. The relevant output:

    Diagram
    CLONE: compare=1 same_handle=0 name=a
    UVM_INFO .../uvm_comparer.svh(575) @ 0: reporter [MISCMP] Miscompare for a.data: lhs = 'h12345678 : rhs = 'h12345679
    UVM_INFO .../uvm_comparer.svh(527) @ 0: reporter [MISCMP] 1 Miscompare(s) for object a@149 vs. a@153
    AFTER EDIT: compare=0
    IDLE DIFFERS: compare=1
    -----------------------------------
    Name    Type       Size  Value
    -----------------------------------
    a       axis_item  -     @149
      data  integral   32    'h12345678
      last  integral   1     'b1
      idle  integral   32    'd3
    -----------------------------------
    OVERRIDE: create gives axis_item_err, clone gives axis_item
    • The clone is equal but not the same handle, and it keeps the original's name, a, because clone() passes get_name() to create.

    • The miscompare names the field, a.data, because do_compare used the comparer. With == you would get only compare=0.

    • A difference in `idle` compares equal, as intended. The field-macro version behaved the same with UVM_NOCOMPARE.

    • The last line is the factory point: with a type override registered, type_id::create built the derived class, but clone() still built an axis_item.

    Why shallow copies bite: a model you can run

    do_copy decides how deep a copy goes. Copy a handle field with = and both objects share the nested object. This plain-SystemVerilog model (no UVM library needed) shows the difference with a packet that holds a header object.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    class hdr;
      bit [7:0] id;
    endclass
    
    class pkt;
      bit [31:0] data;
      hdr        h;
      function new();
        h = new();
      endfunction
      function void copy_shallow(pkt rhs);
        data = rhs.data;
        h    = rhs.h;          // shares the header
      endfunction
      function void copy_deep(pkt rhs);
        data = rhs.data;
        h    = new();
        h.id = rhs.h.id;       // owns its own header
      endfunction
    endclass
    
    module tb_copy_depth;
      pkt orig, s, d;
      int errors = 0;
      initial begin
        orig = new();
        orig.data = 32'hA5;
        orig.h.id = 8'd1;
        s = new(); s.copy_shallow(orig);
        d = new(); d.copy_deep(orig);
        orig.h.id = 8'd7;      // the producer reuses its header
        if (s.h.id != 8'd7) errors++;   // the shallow copy changed with it
        if (d.h.id != 8'd1) errors++;   // the deep copy did not
        if (errors == 0) $display("PASS: shallow copy saw id 7, deep copy kept id 1");
        else $display("FAIL: %0d copy-depth checks wrong", errors);
        $finish;
      end
    endmodule

    Field macros give the same choice through flags: uvm_field_object copies deeply by default, and UVM_REFERENCE makes it a handle copy. Hand-written code makes the choice visible in one line, which is its biggest advantage.

    Why the bench uses clone, and what breaks without it

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // snippet: from the course DMA bench
    // Fault injection without touching the sequence: cuts every packet after beat
    // `cut` by setting TLAST there and dropping the rest of the packet.
    class axis_truncating_driver extends axis_driver;
      `uvm_component_utils(axis_truncating_driver)
      int unsigned cut = 3;           // index of the beat that gets TLAST
      int unsigned beat_no;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function axis_item pre_drive(axis_item it);
        axis_item b;
        int unsigned i = beat_no;
        beat_no = it.last ? 0 : beat_no + 1;    // count beats per packet
        if (i > cut) return null;                // after the cut: drop the beat
        if (i < cut) return it;
        $cast(b, it.clone());                    // never edit the sequence's own item
        b.last = 1'b1;
        `uvm_info("TRUNC", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
        return b;
      endfunction
    endclass

    On the cut beat, the driver needs an item with last = 1. It could simply set it.last = 1, but it belongs to the sequence: the sequence created it, may still hold the handle, and may read it after finish_item returns. Editing it would quietly change the stimulus record, and a sequence that counts beats by last would see a packet end where it never ended one. So the driver clones first and edits the copy, which is only correct because axis_item::do_copy copies every field. Leave do_copy empty and the clone comes back with data = 0: the DMA receives a zero word, and the scoreboard (which predicts from the monitored input) agrees with it, so nothing fails while the test quietly stops testing what it claims to.

    Common pitfall

    Adding a field to an item and forgetting do_copy, do_compare and do_print is the main cost of hand-written methods. Put the field list in one place in the class, review all four methods whenever a field is added, and let a unit test that clones and compares a randomized item catch the miss.

    Which to choose: the trade-off

    Concern

    Field macros

    Hand-written do_*

    Writing it

    One line per field

    Four methods to keep in step

    Run time

    Slower: generic code walks every field for every operation

    Fast: only the work you wrote

    Debugging

    Behaviour hidden in macro expansion and flags

    Plain code you can step through

    Control

    Flags per field (UVM_NOCOMPARE, UVM_REFERENCE, radix)

    Anything: conditional compares, tolerances, custom formats

    Auto-configuration

    Fields can be set from config_db automatically

    Read config explicitly

    A common team policy, and a defensible interview answer: field macros for config objects and low-volume classes, where convenience and auto-configuration help; hand-written methods for sequence items and anything copied or compared millions of times, where speed and explicit behaviour matter.

    Interview

    "Field macros or do_copy and do_compare: which do you use, and why?"

    A weak answer: "Field macros, because they save typing." Or the opposite, "Never macros, they're slow," with nothing behind it.

    A strong answer names the trade-off and a policy: "Macros are compact but generic: every copy or compare walks a field list, and the behaviour lives in flags that are easy to get wrong. For high-volume items I hand-write do_copy, do_compare, do_print and convert2string, using the comparer so miscompares name the field, and I leave stimulus-only fields out of compare. For configs I'm happy with macros. And I remember that clone is create plus copy, not a factory call, so its depth is whatever do_copy implements."

    Expect the follow-ups: Why call super.do_compare? (so derived items extend rather than replace the parent's checks) and Does clone honour a factory override? (no: it builds the same class as the original).

    Check yourself A derived item axis_err_item adds a field corrupt and overrides do_copy without calling super.do_copy. What does clone() return?

    An axis_err_item (clone keeps the dynamic type) with corrupt copied but data, last and idle left at their defaults, because the parent's do_copy never ran. Always call super.do_copy(rhs) first.

    Check yourself Your scoreboard calls exp.compare(act) and a test fails with compare=0 but no MISCMP line in the log. What is the likely cause?

    The item's do_compare uses plain == and returns the result without going through the comparer, so the comparer never learns which field differed. Rewrite it with comparer.compare_field_int (or the other compare_* calls), or have the scoreboard print both items with sprint() when the compare fails.

    Check yourself A colleague clones items in a monitor because "a factory override might change the item type and clone handles that". Is the reasoning right?

    No. clone() builds an object of the same class as the one being cloned; it never consults the factory. If you need the overridden type, create through type_id::create and then copy(). Cloning in a monitor is fine for a different reason: it gives subscribers an object the monitor will not change.

    Next: you can now build the data side of a bench. The stimulus, check and coverage triangle shows how that data serves a testplan.

    Related topics

    • Field macros in detail

    • Scoreboard architecture

    • Driver injection hooks

    Previous

    8 / 280

    Next: The Stimulus, Check, and Coverage Triangle

    Up next

    Up nextThe Stimulus, Check, and Coverage Triangle

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why UVM Exists: History and Goals
    • What UVM Standardizes (and What You Own)
    • Your First UVM Testbench