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
    • UVM Factory: Registration, Overrides and Debug
    • config_db and Resources
      • The set/get Contract: Four Arguments That Must Agree
      • Instance Paths and Wildcards: Who Can See a Setting
      • config_db Resolution: Who Wins at Build and After
      • Config Objects: One Typed Handle Instead of Twenty Knobs
      • config_db Performance and Anti-Patterns
      • Auto-Config and Field Macros on Components
      • Configuring Sequences
  • 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

‹config_db and Resources
  1. Topics
  2. UVM Verification
  3. Factory & Configuration
  4. config_db and Resources
  5. config_db Performance and Anti-Patterns

Part 3 · Factory & Configuration · Intermediate

config_db Performance and Anti-Patterns

What a config_db lookup costs, why gets do not belong in per-transaction code, the price of global * sets, check_config_usage and +UVM_CONFIG_DB_TRACE, and wait_modified for knobs that change at run time.

On this page

    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.

    The idea: a get is a search, not a variable read

    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.

    The picture

    Flow of one config_db get: collect resources by name, filter by type and scope pattern match, sort the candidates by precedence and return the winner.
    What one get does. Every resource with the field name is a candidate to check; each costs a type check and a pattern match. More sets with the same name, broader scopes and more gets all multiply the work.

    Build it: the patterns that keep config cheap

    Get once, in build, and keep the value

    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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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
    design.sv
    1
    2
    3
    4
    5
    6
    // 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));
    endfunction

    Knobs that really change at run time: wait_modified

    Sometimes 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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 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_none

    A run-time set gets precedence 1000, so the new value wins over the build-time setting, as the precedence rules say.

    The anti-patterns, and what to do instead

    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; wait_modified for true run-time knobs.

    set(null, "*", ...) for a field many components read

    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 super.build_phase.

    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.

    check_config_usage() at the end of the run.

    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.

    Finding unused and mismatched settings

    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.

    design.sv
    1
    2
    3
    4
    // snippet: in the base test
    function void report_phase(uvm_phase phase);
      check_config_usage();      // settings with at least one write and no reads
    endfunction

    When 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.

    Watch it run

    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:

    Diagram
    +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 :    0

    What breaks: the knob read on every beat

    A 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.

    Interview

    "Is it OK to call uvm_config_db get inside a driver's run loop?"

    A weak answer: "Yes, it's just a lookup."

    A strong answer: "Not per transaction. A get collects every resource with that field name, type-checks each, glob or regex matches its scope against my full name and sorts by precedence. I get once in build and store a member. For a knob that really changes mid-run I fork a wait_modified loop and get only when it fires. I also avoid set(null, "*") for widely read names, prefer one config object over many knobs, and call check_config_usage at the end of the run to catch sets nobody read."

    Follow-ups: Why call check_config_usage late? (settings read in run_phase would show as unused earlier) and What does +UVM_CONFIG_DB_TRACE print? (a CFGDB/SET or CFGDB/GET line per set and get).

    Check yourself A monitor needs a 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 yourself 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.

    Check yourself Why does a 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.

    Related topics

    • Config objects

    • Auto-config and field macros

    • Resolution order: who wins

    Previous

    57 / 280

    Next: Auto-Config and Field Macros on Components

    Up next

    Up nextAuto-Config and Field Macros on Components

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The set/get Contract: Four Arguments That Must Agree
    • Instance Paths and Wildcards: Who Can See a Setting
    • config_db Resolution: Who Wins at Build and After