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
  • 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
    • Callbacks, Command Line, Seeds and Versions
      • Callbacks Done Right
      • The Command Line in Full
      • Random Stability and Seeds
      • UVM 1.2 to IEEE 1800.2: The Delta Table
      • Performance at Scale
    • Synchronisation and Reporting
    • Testbenches at Scale
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Callbacks, Command Line, Seeds and Versions
  1. Topics
  2. UVM Verification
  3. Advanced UVM in Practice
  4. Callbacks, Command Line, Seeds and Versions
  5. Performance at Scale

Part 10 · Advanced Topics · Intermediate

Performance at Scale

Where UVM benches spend time and memory: field macros, config_db lookups, report formatting, objections, callbacks, analysis fan-out, unbounded queues and recording; measured costs, profilers, compile and checkpoint strategy, and a checklist.

On this page

    The SoC regression has grown to 3,000 tests and takes fourteen hours; the farm budget says eight. Someone proposes dropping a third of the tests. Before anyone deletes coverage, a senior engineer asks for one profile of one long test. It shows a monitor reading a config_db knob on every beat and a scoreboard formatting a string for every compare, at a verbosity nobody runs. Where does a UVM bench really spend its time, and which fixes buy back hours instead of seconds?

    Time goes where work is repeated

    A simulation's time splits three ways: the simulator kernel evaluating the design, your testbench code in class methods, and UVM infrastructure your code calls: string formatting, report filtering, config lookups, objection bookkeeping, object creation, policy-class copies. The design part you rarely control. The other two are where benches waste time, and the waste is almost always one small cost multiplied by the number of transactions or clock edges.

    A restaurant does not get faster by buying a better oven if the waiter walks to the cellar for every glass of water. Profile first to find the walk; then fix the walk, not the oven. Every number below is from a scratch run: one million iterations of each operation inside a run_phase, median of three runs, Verilator 5.048 on an Apple M4 Pro with one thread. Absolute values differ on every simulator; the ratios are the lesson.

    The picture: where one stream beat pays

    Flowchart of the per-beat path: the driver feeds the monitor, which fans out to the scoreboard and coverage; the scoreboard prints a report per compare. Side branches mark per-beat config_db gets, per-item objections and transaction recording as costs.
    Each box runs once per beat. A cost that looks tiny once becomes minutes when it sits on this path in a long test; the three boxes with question marks are the usual offenders.

    Build it: measure, do not guess

    The scratch test runs one operation in a loop, chosen by a plusarg, so one build measures all of them. The interesting part is what each case does:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: scratch run (pf_test); n = 1000000; leaf is a component two levels down
    case (what)
      "copy_macro":   repeat (n) mb.copy(ma);       // 4 fields via uvm_field_int macros
      "copy_do":      repeat (n) db.copy(da);       // the same 4 fields in a hand-written do_copy
      "cfg_get":      repeat (n) begin void'(uvm_config_db#(int)::get(leaf, "", "knob", v)); acc += v; end
      "cfg_cached":   begin void'(uvm_config_db#(int)::get(leaf, "", "knob", v)); repeat (n) acc += v; end
      "info_inside":  repeat (n) `uvm_info("PF", $sformatf("item %s", da.convert2string()), UVM_HIGH)
      "info_outside": repeat (n) begin s = $sformatf("item %s", da.convert2string()); `uvm_info("PF", s, UVM_HIGH) end
      "info_sprint":  repeat (n) begin s = ma.sprint(); `uvm_info("PF", s, UVM_HIGH) end
      "objection":    repeat (n) begin phase.raise_objection(leaf); phase.drop_objection(leaf); end
    endcase

    The config_db holds 22 settings (20 unrelated wildcards plus the one that matches), a small but realistic database. The verbosity is the default UVM_MEDIUM, so every UVM_HIGH message is filtered out.

    Watch it run: the measured costs

    Operation (1,000,000 times)

    Time

    Per call

    What it shows

    Empty loop (baseline)

    0.03 s

    -

    Startup and the loop itself

    copy() with field macros / hand-written do_copy

    0.35 s / 0.28 s

    0.32 / 0.25 µs

    Macros 1.3x slower here

    compare() with field macros / hand-written do_compare

    0.42 s / 0.29 s

    0.39 / 0.26 µs

    Macros 1.5x slower here

    config_db::get / value cached in a variable

    4.5 s / 0.03 s

    4.5 µs / ~0

    Over 100x: a get is a search

    filtered uvm_info, formatting inside the macro

    0.05 s

    0.02 µs

    The macro checks verbosity before formatting

    filtered uvm_info, string built before the macro

    0.42 s

    0.39 µs

    20x: the string is built and thrown away

    filtered uvm_info of sprint()

    4.4 s

    4.4 µs

    200x: the table printer runs for nothing

    raise and drop an objection on a leaf

    0.60 s

    0.57 µs

    Propagation up the hierarchy, per pair

    an empty callback site

    0.18 s

    0.15 µs

    An iterator object per call

    • config_db in a hot path is the biggest single cost: 4.5 µs per get against a 22-entry database. At 50 million beats that is about 225 s of lookups in one test. Get once in build_phase and keep the value; the patterns are in config_db performance.

    • Reports cost when you build the string yourself. Keep the formatting inside the uvm_info arguments; never call sprint() or convert2string() into a variable "just in case". The macro already checks uvm_report_enabled per ID, so a manual verbosity guard adds nothing and breaks +uvm_set_verbosity for that ID.

    • Field macros are slower but not ruinous on this tool. Hand-written do_copy and do_compare were 1.3x to 1.5x faster for four integer fields; the gap grows with arrays, objects and UVM_ALL_ON printing, and varies by simulator. Write the methods for items that are copied or compared per transaction (field macros).

    • Objections belong to the test, once. Raise and drop per transaction and the cost multiplies by every item and by the depth of the hierarchy, since each raise updates every ancestor's total.

    Memory: the leaks a garbage collector cannot see

    SystemVerilog frees an object when nothing refers to it, so UVM benches do not leak in the C sense. They leak by holding references: anything appended and never removed lives until the end of the test. In a long soak test, that is the difference between 2 GB and an out-of-memory kill at hour three.

    • Unbounded analysis FIFOs. uvm_tlm_analysis_fifo is always unbounded (its constructor passes size 0, "analysis fifo must be unbounded"). A FIFO that nobody calls get on grows by one item per write, forever.

    • Scoreboard queues that never drain: predictions for a port that is never compared, or per-ID queues for IDs that never complete. Check sizes in check_phase and report the maximum depth.

    • Storing what the monitor sent: a coverage or debug component that keeps every item "for later". Keep counters, or a bounded window.

    • Transaction recording: recording every item to the waveform database costs time and disk per transaction; enable it per component for debug runs, not in the nightly regression. Recording is covered in Synchronisation and reporting.

    Profilers, compiles and checkpoints

    Measure with the simulator's own profiler before changing code. Synopsys VCS has -simprofile, Cadence Xcelium has -profile, and Siemens Questa and Aldec Riviera-PRO have their own profilers (see the user guide for the switches of your version). Verilator can build with --prof-cfuncs for gprof. Read the result by class method: a UVM library function near the top (uvm_config_db::get, uvm_printer, the field-macro __m_uvm_execute_field_op) is a bench problem in disguise.

    • Compile once, run many. The UVM package is most of every build of the course bench. Use the vendor's precompiled UVM, and partition or incremental compiles so a test change does not rebuild the library.

    • Checkpoint after the expensive start. For SoC tests with a long boot or reset sequence, most simulators can save a snapshot and restart many tests from it (Xcelium and VCS both support save and restart). Check what a restart restores: open files, DPI-side C state and the random state need care.

    • Dump less. Full waveform dumping of a large design can cost more than the whole testbench; dump the scope you are debugging, and only in debug runs.

    • Print less at time 0. The bench's dma_base_test prints the topology in end_of_elaboration_phase; on a 10,000-component SoC bench that table is long and slow, so make it a debug option.

    What breaks: the knob read on every beat

    A stream monitor gained a feature: "if check_parity is set, check parity on each beat." The author read the knob with uvm_config_db#(bit)::get inside the sampling loop, so a test could change it mid-run. On a block bench the tests were short and nobody noticed. At subsystem level, with eight such monitors and long data tests, the per-beat lookups dominated the profile. The measured 4.5 µs per get above puts the order of magnitude in view: eight monitors at a few million beats each is minutes of pure searching per test, repeated across the whole regression.

    The fix took ten minutes: read the knob once in build_phase, and if it really must change at run time, wait on uvm_config_db#(bit)::wait_modified in a separate thread and update a member. The general rule is the lesson: anything that runs per beat must be a member read, not a search, a format, or an allocation you can avoid.

    A performance checklist

    • Profile one long test before changing anything; sort by class method.

    • No config_db::get, factory create of components, or sprint() in per-transaction code.

    • Formatting only inside uvm_info arguments; no manual verbosity guards.

    • Hand-written do_copy and do_compare for items copied or compared per transaction.

    • One objection per test, owned by the test; scoreboards use phase_ready_to_end.

    • Every queue and analysis FIFO has a reader; report maximum depths in check_phase.

    • Callback sites at transaction boundaries; recording and waveform dumps only in debug runs.

    • Precompiled UVM, incremental compiles, and checkpoints for long boots.

    Interview

    "Your regression is too slow. How do you speed it up without dropping tests?"

    A weak answer: "Turn off logging and use a faster machine." Logging that is already filtered costs almost nothing, and hardware does not fix a per-beat search.

    A strong answer: "Profile one representative long test with the simulator's profiler and read it by class method. The usual hot spots are config_db gets in monitors or drivers (a get searches the resource pool: over 100 times the cost of a member read in my measurement), strings built outside uvm_info or sprint() calls at filtered verbosity, field-macro copies and compares per transaction, objections raised per item, and unbounded queues or analysis FIFOs that grow memory. Then the build side: precompiled UVM and incremental compiles, and checkpoints after long boots. Finally regression policy: fewer seeds for tests that have stopped finding bugs, chosen from coverage, not by deleting tests."

    Follow-ups: Is a manual if (get_report_verbosity_level() >= UVM_HIGH) guard worth adding? (no: the macro already checks per ID, and the manual guard ignores ID-specific verbosity) and How do you find a memory leak in a UVM bench? (look for references that are never released: queue and FIFO depths over time, components that store items).

    Check yourself A scoreboard runs string s = exp.sprint(); and then uvm_info("SB", s, UVM_HIGH) on every compare, and the regression runs at UVM_LOW. What does it cost per compare in the scratch measurement, and what is the fix?

    About 4.4 µs per compare, spent building a table that is thrown away, roughly 200 times the cost of a filtered uvm_info whose formatting is inside the macro. Move the formatting into the macro's message argument (or use convert2string() there), so it runs only when the message will print.

    Check yourself A soak test dies after three hours with an out-of-memory error, although no class "leaks". Name two likely places to look in a UVM bench.

    An analysis FIFO (uvm_tlm_analysis_fifo, always unbounded) that nothing reads, and a scoreboard or coverage queue that is appended to and never drained, such as predictions for an output that is never compared. Report queue and FIFO sizes periodically or in check_phase to find which one grows.

    Check yourself Why does raising and dropping an objection per transaction cost more in a deep hierarchy than in a flat one?

    Each raise and drop updates the count of the object and the total of every ancestor up to uvm_top, so the work grows with the depth. In the scratch measurement a pair on a leaf two levels down cost about 0.57 µs. One objection per test, held by the test, avoids the cost entirely.

    Next: the operational levers are done. Continue with Synchronisation and reporting: events, barriers, report catchers, verdict servers and watchdogs.

    Related topics

    • config_db performance

    • Field macros and automation

    • Objections at scale

    • TLM FIFOs and debug

    Previous

    173 / 280

    Next: Synchronisation and Reporting

    Up next

    Up nextSynchronisation and Reporting

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Callbacks Done Right
    • The Command Line in Full
    • Random Stability and Seeds