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
    • SVA Fundamentals
    • Sequences: Temporal Building Blocks
    • Implication and Property Operators
    • Sampled Value Functions
    • Protocol Assertion Cookbook
    • Advanced SVA: Local Variables, Multiclock & More
    • Assertion Debug & Methodology
      • Debugging Assertion Failures
      • When Passing Is the Bug: Vacuity & Coverage
      • Assertion Performance & Noise Control
      • Where Assertions Live
      • The Assertion Review Checklist
    • SVA Interview Drills
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Assertion Debug & Methodology
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Assertions — Interview-Critical Deep Dive
  4. Assertion Debug & Methodology
  5. Assertion Performance & Noise Control

Part 4 · Assertions (SVA) · Intermediate

Assertion Performance & Noise Control

Control assertion cost with precise antecedents, bounded windows, and quiet logs.

On this page

    Why this matters

    A regression slowed fourfold after one assertion was added. Its antecedent was 1'b1 and its window was 100 cycles, so every cycle started 100 threads.

    After this lesson you can: find the assertions that cost the most, bound liveness for simulation, and keep failure reports readable.

    Where assertion time goes

    A concurrent assertion starts a new attempt every clock edge. Each attempt can fork threads at every range and branch, and the simulator pays for live threads.

    Cost is roughly attempts past the antecedent, times threads per attempt, times thread lifetime. Weak antecedents and unbounded windows dominate all three factors.

    Form

    Attempts start

    Threads alive

    Work grows with

    1'b1 |-> ##[1:100] (done && tag_ok)

    Every edge

    Up to 100 per attempt, all the time

    Simulated cycles

    $rose(start) |-> ##[1:100] (done && tag_ok)

    Only when a transaction starts

    Up to 100 per transaction

    Transactions

    • Strengthen antecedents: $rose(start), not 1'b1 or a level held through the burst.

    • Bound every range with a real protocol bound, never ##[1:$].

    • Use first_match where any single match suffices; fewer threads survive.

    • Prefer one assertion with a precise trigger over a cycle-invariant form recomputed each edge.

    Common pitfall

    An antecedent of 1'b1 or an always-true level starts an attempt every cycle and forks a full window of threads each time. It also makes vacuity statistics meaningless, since the antecedent can never be false.

    Bounded liveness

    Unbounded threads never die while unresolved. If a buggy design drops responses, each lost response leaks a permanent live thread, the run slows progressively, and the failure reports at end of simulation, far from the cause.

    In simulation, replace liveness with bounded liveness. The listing below is not run in this course's checks: it is a fragment, and Verilator 5.048 does not support s_eventually. The testbench proves the bounded form instead.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // not-run: fragment without a module; Verilator 5.048 does not support s_eventually
    // LEAKY: one thread per lost response lives until end of sim,
    // and the failure only reports at sim end
    ap_done_leaky: assert property (@(posedge clk) disable iff (!rst_n)
      $rose(start) |-> s_eventually done);
    
    // BOUNDED: worst-case-plus-margin from the spec; fails AT the
    // timeout, near the cause, and the thread always terminates
    localparam int MAX_LAT = 200;
    ap_done: assert property (@(posedge clk) disable iff (!rst_n)
      $rose(start) |-> ##[1:MAX_LAT] done)
      else $error("done timeout: more than 200 cycles after start");

    A generous but finite window is both faster and a better bug report. Reserve unbounded liveness for formal tools, behind a parameter.

    Check yourself Why does s_eventually report far from the cause in simulation?

    Its thread stays live until the run ends, so a strong liveness failure only surfaces at end of simulation. A bounded window fails at the timeout, next to the dropped response.

    Phase control and severity

    During reset and reconfiguration, rules are legitimately violated. Scoped controls silence exactly those phases, and re-enable after settle.

    The listing below is a fragment that names a tb.dut hierarchy, so it is not run in this course's checks. $assertoff at time zero is enough because nothing is in flight yet. Around a mid-run reset use $assertkill, which aborts in-flight attempts and then stays off until $asserton.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // not-run: fragment; tb.dut, tb.rst_n and program_registers are not defined here
    initial begin
      $assertoff(0, tb.dut);                // X-soup phase: total silence
      wait (tb.rst_n);
      repeat (2) @(posedge tb.clk);
      $asserton(0, tb.dut);                 // main phase: everything on
    end
    
    task automatic reconfigure_link();
      $assertoff(0, tb.dut.u_link);         // rules legitimately violated
      program_registers();                  // during reconfig
      @(posedge tb.clk);
      $asserton(0, tb.dut.u_link);
    endtask

    Severity is routing information for the regression script. The wrong level misroutes triage.

    Level

    Meaning

    Regression effect

    $fatal

    Corruption is spreading; nothing after this is meaningful

    Stops the run; use sparingly

    $error

    Real protocol violation; the test keeps running

    Fails the test; the default for asserts

    $warning

    Suspicious but legal, or a waiver under study

    Must not fail the test

    $info

    Debug aid only

    Kept out of regression logs

    Cap repeated failures but keep the count. One broken wire firing every cycle must not bury the second, different bug sitting a thousand lines lower in the log.

    Interview

    Likely questions: “why is 1'b1 |-> ##[1:100] done expensive” and “why avoid s_eventually in simulation”. Answer: an attempt every edge forks up to 100 threads, so work scales with cycles times window instead of transactions. Unbounded threads never die, leak on every lost response, and report at end of run. Severity discipline rounds out the senior answer.

    Testbench

    A source with six-cycle latency and a freeze input. Phase A arrives on time, phase B freezes fourteen edges, phase C moves data under backpressure twelve times.

    latency_src.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
    module latency_src #(
      parameter int LAT = 6
    ) (
      input logic clk, rst_n,
      input logic start, block_ack,
      output logic done
    );
      localparam logic [3:0] LAT_CNT = LAT;
      logic [3:0] cnt;
      logic busy;
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          cnt <= '0;
          busy <= 1'b0;
          done <= 1'b0;
        end else begin
          done <= 1'b0;
          if (start && !busy) begin
            busy <= 1'b1;
            cnt <= LAT_CNT;
          end else if (busy && !block_ack) begin
            if (cnt == 4'd1) begin
              busy <= 1'b0;
              done <= 1'b1;
            end else begin
              cnt <= cnt - 4'd1;
            end
          end
        end
      end
    endmodule
    
    Terminal
    caught done timeout at 235000
    caught data moved under backpressure, occurrence 1
    caught data moved under backpressure, occurrence 2
    caught data moved under backpressure, occurrence 3
    PASS: timeout fired at +10, a 1'b1 antecedent would start 14 attempts, noise capped at 3 of 12

    Remember

    • Precise antecedents make work scale with transactions, not cycles.

    • Bound every window with a protocol limit.

    • Rate-limit repeated reports; the first one matters.

    Rule of thumb: an assertion's cost is attempts times threads times lifetime: shrink all three.

    How this was checked

    Run with Verilator 5.048 (--binary --timing --assert, plus --coverage when a listing has a cover property) on 2026-10-18; every bench printed the PASS line shown. Verilator stops at the first $error or failing assertion without an action block, so the benches count failures in the action block. Icarus 13 has no concurrent assertions.

    Related topics

    • When Passing Is the Bug

    Previous

    130 / 259

    Next: Where Assertions Live

    Up next

    Up nextWhere Assertions Live

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Debugging Assertion Failures
    • The Assertion Review Checklist