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?
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 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:
// 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
endcaseThe 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.
Operation (1,000,000 times) | Time | Per call | What it shows |
|---|---|---|---|
Empty loop (baseline) | 0.03 s | - | Startup and the loop itself |
| 0.35 s / 0.28 s | 0.32 / 0.25 µs | Macros 1.3x slower here |
| 0.42 s / 0.29 s | 0.39 / 0.26 µs | Macros 1.5x slower here |
| 4.5 s / 0.03 s | 4.5 µs / ~0 | Over 100x: a get is a search |
filtered | 0.05 s | 0.02 µs | The macro checks verbosity before formatting |
filtered | 0.42 s | 0.39 µs | 20x: the string is built and thrown away |
filtered | 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.
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.
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.
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.
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.
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.
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.
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.