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 Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
    • DPI-C: Calling C from SystemVerilog
    • bind: Attaching Verification to RTL
    • Macros, Defines & Compilation
    • Plusargs, Files & Runtime Configuration
    • Simulation Performance & TB Memory
      • Where Simulation Time Goes
      • Testbench Hotspots
      • TB Memory Discipline
      • Visibility Cost and Debug Strategy
    • RTL/TB Boundaries, Races & Timing
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Simulation Performance & TB Memory
  1. Topics
  2. SystemVerilog
  3. Advanced Language & Integration
  4. Simulation Performance & TB Memory
  5. Testbench Hotspots

Part 7 · Advanced & Integration · Intermediate

Testbench Hotspots

Polling loops, string building, copy storms, sample storms and solver abuse, with fixes.

On this page

    Wait on the event, not the clock

    A loop that wakes every cycle to test a condition forces the scheduler to run the process millions of times for zero information. The cure is to wait on the event itself, so the process wakes exactly when the condition becomes true.

    A level condition needs a level wait. If done is already 1, a polling loop exits at once while @(posedge done) hangs waiting for the next edge. Use wait with a level comparison for a level condition, and @(posedge done) only when the edge is guaranteed in the future.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // snippet
    // BEFORE: wakes every cycle for the whole test
    task automatic wait_for_done_polling();
      while (done !== 1'b1) @(posedge clk);
    endtask
    
    // AFTER (level condition): blocks until the expression holds, even if set now
    task automatic wait_for_done_level();
      wait (done === 1'b1);
    endtask
    
    // AFTER (edge guaranteed in the future): wakes exactly once on the edge
    task automatic wait_for_done_edge();
      @(posedge done);
    endtask
    • A ten-million-cycle test with one polling loop means ten million scheduler wake-ups.

    • Five polling monitors means five processes thrashing the scheduler every cycle.

    • @(posedge sig) and wait(expr) cost nothing while the condition is false. The kernel owns the sensitivity.

    • Profile symptom: a monitor or scoreboard task near the top with huge call counts.

    Guard before formatting and copy once

    String formatting allocates on every call, even when the message is filtered out and never prints. Guard with the verbosity comparison before formatting, never after.

    Deep copies behave the same way. Copy the transaction once at the observation point and pass the handle downstream as read-only.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet
    // BEFORE: formats the string even when nothing prints
    msg = $sformatf("saw txn id=%0d at %0t", id, $time);
    if (verbosity >= 300) $display("%s", msg);   // usually false
    
    // AFTER: the guard runs before the format, so the format often never runs
    if (verbosity >= 300) $display("saw txn id=%0d at %0t", id, $time);

    Plain classes have a built-in shallow copy: tr_frozen = new tr; copies every property, including arrays and queues by value. Only nested object handles stay shared, so nested objects need a hand-written deep copy. Compare and string conversion must be written by hand.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet
    class bus_txn;
      bit [31:0] addr;
      bit [7:0] payload[$];
    endclass
    // Observation point: tr_frozen = new tr;   // built-in shallow copy
    // No nested objects here, so the shallow copy is a full copy.
    // Pass tr_frozen downstream as read-only.

    Sample per transaction and solve narrowly

    A covergroup sampled on every clock fires millions of times whether or not anything interesting happened. Sampling once per completed transaction keeps the same information for a fraction of the calls.

    The per-transaction covergroup below is not run in this course's checks: Verilator 5.048 ignores covergroups and Icarus 13 has none.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; fragment with no module
    covergroup cg with function sample(bit [7:0] addr);
      cp_addr : coverpoint addr {
        bins lo = {[0:127]};
        bins hi = {[128:255]};
      }
    endgroup
    // A monitor calls cg.sample(pkt_addr) once per completed transaction.

    A full constraint solve per loop iteration has the same shape: heavy work repeated for fields that barely change. Solve fully once, then re-solve only the fields that vary, or pre-generate a pool outside the hot loop.

    The narrow randomize listing below is a fragment of statements, so it is not run in this course's checks.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // not-run: fragment: statements only, no class or module
    // Full solve once, outside the hot loop:
    void'(big_txn.randomize());
    // Narrow re-solve per iteration (only addr and len vary):
    void'(big_txn.randomize(addr, len));
    // Alternative: pre-generate a pool of solved transactions before the loop
    // and draw from it inside the loop.

    Symptom in a profile

    Likely cause

    Fix

    Monitor task, huge call count

    Polling loop

    @(edge) or wait(expr)

    String formatting high in profile

    Hot-path formatting

    Guard before format

    Allocator or new() high

    Per-hop deep copies

    Copy once, share handle

    Covergroup sample high

    Per-clock sampling

    Per-transaction sample(t)

    Solver time high

    randomize in tight loop

    Narrow randomize or pool

    Common mistakes

    Common pitfall

    Replacing a polling loop with @(posedge done) when done may already be 1 trades a slow test for a hung test. The edge form waits for the next edge, which never comes. Match the wait to the condition: wait with a level comparison for a level, @(posedge) for a guaranteed future edge.

    Interview

    Likely question: “Rewrite this polling loop with an event wait, and say when your rewrite hangs.”

    • Level condition: wait with a level comparison returns at once if already true.

    • Edge form: @(posedge done) hangs when done is already 1, since no new edge arrives.

    • Rule: use the edge form only when the edge is guaranteed in the future.

    Check yourself A monitor formats every transaction with $sformatf, then checks verbosity before printing. The profile shows formatting near the top. What is wrong?

    The format runs before the guard, so it allocates on every transaction even though the result is usually discarded. Moving the verbosity check before the format removes the cost in quiet runs.

    Check yourself done is already 1 when a task runs @(posedge done). What happens, and what was the correct wait?

    The task hangs until the next rising edge, which may never come. A level wait such as wait (done === 1'b1) returns at once, since the condition already holds.

    Testbench

    Two small testbenches prove the two runnable ideas: the level-vs-edge wait pair, and a guarded log plus a manual per-transaction coverage counter with a pre-generated pool.

    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
    timeunit 1ns; timeprecision 1ps;
    
    module tb_handshake;
      logic clk = 0;
      always #5 clk = ~clk;
      logic done = 0;
      bit level_ok = 0;
      bit edge_ok = 0;
      int errors = 0;
    
      initial begin
        done = 1;
        wait (done === 1'b1);   // level already holds: returns at once
        level_ok = 1;
        done = 0;
        fork
          begin #37; done = 1; end
          begin @(posedge done) edge_ok = 1; end   // edge guaranteed: caught
        join
        if (!(level_ok && edge_ok)) errors++;
        if (errors == 0) $display("PASS: level wait returned at once and edge wait caught the future posedge");
        else $fatal(1, "FAIL: level_ok=%0d edge_ok=%0d", level_ok, edge_ok);
        $finish;
      end
    endmodule
    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
    module tb_guarded_log;
      int unsigned verbosity = 100;   // regression-quiet: HIGH (300) lines stay off
      int fmt_calls = 0;
      int unsigned cov_small = 0;
      int unsigned cov_med = 0;
      int unsigned cov_large = 0;
      int unsigned pool [64];
      int errors = 0;
    
      function string fmt_msg(int unsigned id);
        fmt_calls++;
        return $sformatf("txn id=%0d", id);
      endfunction
    
      task automatic sample_len(int unsigned len);
        if (len <= 8) cov_small++;
        else if (len <= 64) cov_med++;
        else cov_large++;
      endtask
    
      initial begin
        for (int j = 0; j < 64; j++) pool[j] = j * 2;   // pool of even lens 0..126
        for (int i = 0; i < 1024; i++) begin
          if (verbosity >= 300) $display("%s", fmt_msg(i));   // guarded: never formats
          sample_len(pool[i % 64]);                           // manual per-txn sample
        end
        if (fmt_calls !== 0) errors++;
        if (cov_small !== 80) errors++;
        if (cov_med !== 448) errors++;
        if (cov_large !== 496) errors++;
        if (errors == 0) $display("PASS: guard held at zero formats and bins counted 80, 448 and 496");
        else $fatal(1, "FAIL: fmt=%0d small=%0d med=%0d large=%0d", fmt_calls, cov_small, cov_med, cov_large);
        $finish;
      end
    endmodule

    Related topics

    • Classes and handles — object costs and handle discipline

    Previous

    204 / 259

    Next: TB Memory Discipline

    Up next

    Up nextTB Memory Discipline

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Visibility Cost and Debug Strategy