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.
// 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);
endtaskA 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.
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.
// 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.
// 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.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.
// 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.
// 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 |
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.
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.
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.
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
endmodulemodule 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