An SoC bench with a few hundred agents takes minutes of wall-clock time before the first clock edge, all of it at simulation time 0. A profiler points at the resource pool: string matching, millions of times. Elsewhere in the same bench, a monitor re-reads a check_enable knob from config_db for every transaction, and the run phase is noticeably slower than the same test with the line removed. config_db is convenient enough that nobody thinks about its cost, until it is the bottleneck.
A variable read is one memory access. A config_db get is a library search: find every setting with that field name, check each one's type, match each one's scope pattern against your full name (globs are turned into regular expressions), then sort the survivors by precedence. Think of it as asking a receptionist to search a filing cabinet by hand. Doing that once when you start the job is fine. Doing it for every letter you write is not.
The cost grows with two numbers: how many settings share a field name, and how many lookups happen. Broad scopes such as "*" put a candidate in front of every lookup of that name, everywhere in the bench, and auto-config makes every component look up every field-macro name it declares.
Every get in the DMA bench runs once per component, in build_phase, and stores the result in a member. The drivers read vif once; the env's is_active setting is read once by the agent. Per-transaction code then uses the member:
// bad-example: a lookup for every transaction
task run_phase(uvm_phase phase);
forever begin
axis_item it;
collect(it);
if (uvm_config_db#(bit)::get(this, "", "check_enable", check_enable) && check_enable)
ap.write(it);
end
endtask// snippet: read once in build_phase, use the member in the loop
bit check_enable = 1;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db#(bit)::get(this, "", "check_enable", check_enable));
endfunctionSometimes a value must change mid-run, for example a test that switches the input stream from bursty to back-to-back halfway through. Polling with get is the expensive way. wait_modified is a task that blocks until a set touches that field for your scope; then you get once:
// snippet: a driver that follows a run-time knob without polling
int unsigned max_idle = 8;
task watch_max_idle();
forever begin
uvm_config_db#(int unsigned)::wait_modified(this, "", "max_idle");
void'(uvm_config_db#(int unsigned)::get(this, "", "max_idle", max_idle));
`uvm_info("CFG", $sformatf("max_idle is now %0d", max_idle), UVM_MEDIUM)
end
endtask
// started from run_phase with: fork watch_max_idle(); join_noneA run-time set gets precedence 1000, so the new value wins over the build-time setting, as the precedence rules say.
Anti-pattern | Why it hurts | Instead |
|---|---|---|
get in a per-transaction loop | A search per item, in the hottest code. | Get in build into a member; |
| Every lookup of that name anywhere sees and matches it; reach is unreadable. | Narrow scopes, or one config object handed down. |
Twenty separate knobs per agent | Twenty searches per agent, twenty chances to mistype. | One config object, one get. |
Field macros on every member of every component | Auto-config looks up each field name during | Field macros only on members meant to be configured; see Auto-config. |
Sets nobody reads | Cost with no effect, and usually a typo hiding a bug. |
|
One more cost is memory. Setting the same context, scope and field again reuses one resource and simply moves it to the front, so a knob updated a thousand times is still one entry. Building scope strings with an index ($sformatf("env.agt%0d.*", i) inside a loop that runs every transaction) creates a new resource each time, and the pool, and every later lookup of that name, keeps growing.
check_config_usage() lists every resource that was written but never read. Most entries are typos in a field name or a path, which makes it the cheapest config bug detector UVM has. Call it late: the bench's dma_rand_test reads its job in run_phase, so a check at end of elaboration would wrongly list that setting.
// snippet: in the base test
function void report_phase(uvm_phase phase);
check_config_usage(); // settings with at least one write and no reads
endfunctionWhen a setting exists but is not read, the report's header line is ::: The following resources have at least one write and no reads ::: (ID CFGNRD), followed by each resource's scope, name and value. For tracing every set and get as it happens, run with +UVM_CONFIG_DB_TRACE: each set prints a CFGDB/SET message and each get a CFGDB/GET message, with the scope, field, type, accessor and value. The trace is verbose; turn it on for a single failing test, not a regression.
The DMA bench follows every rule above, so its config costs a few dozen lookups at time 0 and none during the run. Its runs finish in a fraction of a second of wall-clock time on Verilator; the simulated end time is set by the traffic, not by config:
+UVM_TESTNAME=dma_smoke_test
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0A team added an inject_rate knob to their stream driver so tests could vary error injection mid-run. To make changes take effect immediately, the driver called uvm_config_db#(int)::get before every beat. It worked, and the long regressions got slower. The bench also had a global set(null, "*", "inject_rate", 0) in the base test, "so everyone has a default", which meant the lookup always had a candidate to pattern-match, for every beat, on every agent.
Profiling made the cost visible; the fix took ten lines. The driver read inject_rate once in build_phase and started a wait_modified watcher like the one above. The global * set became a member default in the driver. Behaviour was identical, and the run phase stopped paying for config.
coverage_on bit that never changes during a run. Where should it read it?Once, in build_phase, into a member with a sensible default, with a non-fatal get. The run loop uses the member. A value that never changes has no reason to be looked up again.
check_config_usage() in end_of_elaboration_phase reports the job setting from dma_replay_test as unused. Is it a bug?No, it is a timing artefact: dma_rand_test reads job in run_phase, after end of elaboration. Call check_config_usage() in report_phase (or late in the run) so settings read at run time count as read.
set(null, "*", "timeout", ...) slow down components that never read timeout from config_db?It does not slow components that never look up timeout. The cost lands on every lookup of that name: every get for timeout, and, with auto-config, every component that declares a field macro named timeout. Each of those lookups must pattern-match the * resource. The broader problem is reach: the set reaches components nobody intended.
Next: auto-config is the lookup you did not write. Auto-config and field macros on components shows where it happens, and what breaks when super.build_phase is skipped.