Part 4 · Assertions (SVA) · Intermediate

Assertion Performance & Noise Control

Attempt explosion from weak antecedents, the cost of unbounded ranges, reset-phase $assertoff, severity discipline, and readable regressions.

Where assertion CPU time actually goes

A concurrent assertion starts a new evaluation attempt every clock edge , and each attempt may fork threads at every range and or. The simulator pays for live threads, so the cost of an assertion is roughly: attempts that get past the antecedent, times threads per attempt, times average thread lifetime. All three factors are under your control — and the worst offenders are always weak antecedents and unbounded windows.

diagram
ATTEMPT/THREAD EXPLOSION — same check, two costs

  WEAK:    1'b1 |-> ##[1:100] (done && tag_ok)
  cycle    :  0    1    2    3    4   ...
  attempts :  A0   A1   A2   A3   A4  ...   ← antecedent always true:
  threads  :  A0 spawns up to 100 threads        EVERY edge starts work
              A1 spawns up to 100 threads        ~100 live threads per
              ...                                attempt, all cycles, forever

  PRECISE: $rose(start) |-> ##[1:100] (done && tag_ok)
  start    :  0    1    0    0    0   ...
  attempts :  --   A1   --   --   --        ← work only when a real
  threads  :       A1: up to 100 threads      transaction begins

  Same protocol rule. The weak form does ~N_cycles times more work —
  and drowns the thread viewer when you debug it.
  • Strengthen antecedents: $rose(start), not 1'b1 or a level that holds for the whole burst.

  • Bound every range: ##[1:64] with a real protocol bound, not ##[1:$] — unbounded threads from lost responses live until end of sim.

  • first_match() collapses multi-match sequences when any single match suffices — fewer surviving threads.

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


Unbounded ranges — the slow leak

##[1:$] and s_eventually threads never die while unresolved. If responses are occasionally dropped by a buggy DUT (exactly when you need assertions most), every lost response leaks a permanent live thread; a long soak test accumulates thousands, simulation slows progressively, and the failure — if liveness ever resolves — reports at end of test, far from the cause. In simulation, replace liveness with bounded liveness : a generous but finite window is both faster and a better bug report.

systemverilog
// LEAKY: thread per lost response lives until end of sim,
// failure (via strong operator) only reported at sim end
ap_done_leaky: assert property (@(posedge clk) disable iff (!rst_n)
  $rose(start) |-> s_eventually done);

// BOUNDED: pick 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: > %0d cycles after start", MAX_LAT);

Phase control and severity discipline

$assertoff during reset and configuration

systemverilog
initial begin
  $assertkill(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

  • $fatal — corruption is spreading and nothing after this is meaningful (clock dead, scoreboard poisoned). Stops the sim; use sparingly.

  • $error — real protocol violation; test should FAIL but can usually keep running to expose more. The default for assertions.

  • $warning — suspicious but legal, or a known waiver under investigation. Must not fail the test, must not be the dumping ground for demoted errors.

  • $info / pass-action prints — debug aid only; keep out of regression logs (or PassOff them via $assertcontrol).

  • The regression script greps these levels to decide pass/fail — wrong severity literally misroutes triage.

Rate-limiting repeated failures

systemverilog
// One broken signal can fire every cycle for a million cycles.
// Cap the noise, keep the count, never hide the first occurrences.
int unsigned fail_cnt;
ap_stable: assert property (@(posedge clk) disable iff (!rst_n)
  valid && !ready |=> $stable(data))
  else begin
    fail_cnt++;
    if (fail_cnt <= 10)
      $error("data moved under backpressure (occurrence %0d)", fail_cnt);
    else if (fail_cnt == 11)
      $error("data-stability: further failures suppressed (see final count)");
  end
final if (fail_cnt > 10)
  $display("ap_stable total failures: %0d", fail_cnt);

Keeping regressions readable

The purpose of all this discipline is a regression log a human can triage in minutes: the first failure of each distinct check, with sampled values and a timestamp near the cause, and nothing else. A thousand-line wall of repeated identical failures hides the second, different bug sitting at line 970.

diagram
NOISY LOG (untriagable)              DISCIPLINED LOG (triagable)
  ──────────────────────────           ─────────────────────────────
  1150ns ERROR ap_stable ...           1150ns ERROR ap_stable data moved
  1160ns ERROR ap_stable ...                  under backpressure (1)
  1170ns ERROR ap_stable ...           ...occurrences 2..10...
  ...  997 more identical  ...         1260ns ERROR ap_stable further
  9870ns ERROR ap_fifo_ov  ◄─buried           failures suppressed
  ...                                  9870ns ERROR ap_fifo_ov push
                                              while full cnt=16  ◄─visible
  2 bugs, 1 findable                   2 bugs, 2 findable

Interview angle

Performance questions test whether you connect SVA semantics to cost: "Why is 1'b1 |-> ##[1:100] done expensive?" — an attempt every edge gets past the trivial antecedent and forks up to 100 threads, so work scales with cycles times window instead of with transactions. "Why avoid s_eventually in simulation?" — unresolved threads never die, lost responses leak threads for the rest of the run, and the failure reports at end-of-sim instead of at a timeout near the cause. Severity discipline ($error fails the test and continues; $fatal stops; $warning must never hide demoted errors) rounds out the senior answer.

Key takeaways

  • Assertion cost = attempts past the antecedent, times threads per attempt, times thread lifetime — tune all three.

  • Precise edge-triggered antecedents ($rose) cut attempt count from every-cycle to per-transaction.

  • Replace unbounded liveness with bounded windows in simulation — faster, and the failure lands near the cause.

  • $assertkill before reset, $asserton after settle, scoped $assertoff for reconfig/error-injection phases.

  • Rate-limit repeating failures but keep counts — one broken wire must not bury the second bug.

Common pitfalls

  • Antecedent 1'b1 or an always-true level — attempt explosion and vacuity statistics become meaningless.

  • ##[1:$] on a response that a buggy DUT can drop — permanent thread leak, progressive slowdown.

  • $fatal on recoverable protocol errors — one failure hides every other bug the test would have exposed.

  • Demoting noisy $error to $warning instead of fixing the noise — regressions go green while the bug remains.

  • Forgetting $asserton after a scoped $assertoff — that checker is silently dead for the rest of the run.