# Heavy benchmark testbenches
The four-mode heavy suite is designed to include meaningful DUT activity and
host-side verification work. Pure SystemVerilog, C++ DPI, raw C++ VPI, and
Cocotb use the same RTL, stimulus functions, edge sequence, scoreboards, and
checksums.
The C++ DPI testbench remains ordinary explicit signal access and scheduling.
Generated bindings and DPI transport are outside the user-facing file.
:::{note}
These benchmark testbenches predate the standard write model and run as
legacy immediate-write builds, matched to their cocotb and VPI peers — which
is why they drive off `FallingEdge` and settle with explicit delays. They are
measurement peers, not authoring templates: for the shape to copy, see the
[counter example](counter.md) and
[the write model](../scheduling.md#the-write-model).
:::
Run the semantic smoke matrix or reproduce the scaled measurements with:
```sh
make framework-comparison-heavy-test
make framework-comparison-heavy-benchmark
```
The tabbed blocks below are excerpts from the complete runnable testbench
files. The commands build each full testbench, its simulator wrapper, and the
shared RTL before executing it.
## Latest measurements
The July 14, 2026 local reference run used Verilator 5.050 and Cocotb 2.0.1.
The table reports median whole-process wall time over four serialized,
mode-rotated samples after one warm-up per mode. Ratios are normalized to the
exact matching pure SystemVerilog testbench in each row.
| Workload | Work units | Pure SV | cpptb (C++ DPI) | C++ VPI | Cocotb |
|---|---:|---:|---:|---:|---:|
| 32-tap streaming FIR | 100,000 samples | 47.8 ms / 1.00x | 84.1 ms / **1.76x** | 120.2 ms / **2.51x** | 2684.5 ms / **56.16x** |
| Variable-length packet CRC32 | 2,000 packets | 41.1 ms / 1.00x | 70.1 ms / **1.70x** | 117.4 ms / **2.86x** | 2281.2 ms / **55.51x** |
| 4x4 signed matrix accelerator | 2,000 blocks | 36.5 ms / 1.00x | 66.4 ms / **1.82x** | 103.3 ms / **2.83x** | 1989.1 ms / **54.50x** |
Every measured sample reported identical transactions, checks, simulation
cycles, checksums, and zero failures across all four modes. The observed
one-minute system load average was 3.10-3.46. These are machine-specific
reference values; see [Performance](../performance.md#heavy-four-mode-comparison)
for the methodology and interpretation.
## Streaming FIR
The testbench keeps an independent 32-sample history and computes the expected
signed result before driving each sample. These are the equivalent process
bodies from all four benchmark modes:
cpptb (C++ DPI)
```cpp
Task run_fir(Context& context) {
std::array history{};
for (uint32_t iteration = 0; iteration < context.iterations; ++iteration) {
const int16_t sample = fir_sample(iteration);
int64_t expected = static_cast(sample) * fir_coefficient(0);
for (uint32_t tap = 1; tap < history.size(); ++tap) {
expected += static_cast(history[tap - 1]) *
fir_coefficient(tap);
}
for (uint32_t tap = history.size() - 1; tap > 0; --tap) {
history[tap] = history[tap - 1];
}
history[0] = sample;
co_await FallingEdge{context.dut.clk};
context.dut.fir_in_sample.set(static_cast(sample));
context.dut.fir_in_valid.set(1);
co_await RisingEdge{context.dut.clk};
co_await Delay{1_ps};
const uint32_t result = context.dut.fir_out_result.get();
check(context, "FIR result", result, static_cast(expected));
fold(context, result);
++context.result.transactions;
}
context.dut.fir_in_valid.set(0);
check(context, "FIR accepted sample count",
context.dut.fir_sample_count.get(), context.iterations);
}
```
C++ VPI
```cpp
Task run_fir(Context& context) {
std::array history{};
for (uint32_t iteration = 0; iteration < context.iterations; ++iteration) {
const int16_t sample = static_cast(stimulus(iteration));
int64_t expected = static_cast(sample) * fir_coefficient(0);
for (uint32_t tap = 1; tap < history.size(); ++tap)
expected += static_cast(history[tap - 1]) *
fir_coefficient(tap);
for (uint32_t tap = history.size() - 1; tap > 0; --tap)
history[tap] = history[tap - 1];
history[0] = sample;
co_await FallingEdge{context.dut.clk};
context.dut.fir_in_sample.set(static_cast(sample));
context.dut.fir_in_valid.set(1);
co_await RisingEdge{context.dut.clk};
co_await Delay{1_ps};
const uint32_t result = context.dut.fir_out_result.get();
check(context, "FIR result", result, static_cast(expected));
fold(context, result);
++context.result.transactions;
}
context.dut.fir_in_valid.set(0);
check(context, "FIR accepted sample count",
context.dut.fir_sample_count.get(), context.iterations);
}
```
Cocotb
```python
async def run_fir(dut, state):
history = [0] * 32
for iteration in range(state.iterations):
sample = signed16(stimulus(iteration))
expected = sample * fir_coefficient(0)
expected += sum(
history[tap - 1] * fir_coefficient(tap)
for tap in range(1, 32)
)
history[1:] = history[:-1]
history[0] = sample
await FallingEdge(dut.clk)
dut.fir_in_sample.value = sample & 0xFFFF
dut.fir_in_valid.value = 1
await RisingEdge(dut.clk)
await Timer(1, unit="ps")
result = int(dut.fir_out_result.value)
state.check("FIR result", result, expected)
state.fold(result)
state.transactions += 1
dut.fir_in_valid.value = 0
state.check(
"FIR accepted sample count",
int(dut.fir_sample_count.value),
state.iterations,
)
```
Pure SystemVerilog
```systemverilog
task automatic run_fir();
logic signed [15:0] history [0:31];
logic signed [15:0] sample;
longint signed expected;
history = '{default: '0};
for (int unsigned iteration = 0; iteration < iterations; iteration++) begin
sample = fir_sample(iteration);
expected = sample * fir_coefficient(0);
for (int tap = 1; tap < 32; tap++)
expected += history[tap - 1] * fir_coefficient(tap);
for (int tap = 31; tap > 0; tap--) history[tap] = history[tap - 1];
history[0] = sample;
@(negedge clk);
fir_in_sample = sample;
fir_in_valid = 1'b1;
@(posedge clk);
#1ps;
check32("FIR result", fir_out_result, expected[31:0]);
fold(fir_out_result);
transactions++;
end
fir_in_valid = 1'b0;
check32("FIR accepted sample count", fir_sample_count, iterations);
endtask
```
The default run processes 100,000 samples, so both the DUT and reference model
perform 3.2 million multiply-accumulate terms.
## Packet CRC32
Each iteration is one 32-95 byte packet. The reference CRC is updated for
every byte while `crc_in_last` defines the frame boundary:
```cpp
for (uint32_t packet = 0; packet < context.iterations; ++packet) {
uint32_t expected = 0xffff'ffffu;
const uint32_t length = packet_length(packet);
for (uint32_t offset = 0; offset < length; ++offset) {
const uint8_t data = packet_byte(packet, offset);
expected = crc32_byte(expected, data);
co_await FallingEdge{context.dut.clk};
context.dut.crc_in_data.set(data);
context.dut.crc_in_last.set(offset + 1u == length);
context.dut.crc_in_valid.set(1);
co_await RisingEdge{context.dut.clk};
}
co_await Delay{1_ps};
const uint32_t result = context.dut.crc_out_result.get();
check(context, "packet CRC32", result, ~expected);
fold(context, result);
}
```
The variable frame length prevents the run from reducing to one fixed-period
transaction shape.
## Matrix accelerator
Each block loads two signed 4x4 matrices, pulses `mat_start`, then validates
the ordered output stream:
```cpp
for (uint32_t index = 0; index < 16; ++index) {
matrix_a[index] = matrix_value(block, 0, index);
co_await load_matrix_value(context, 0, index, matrix_a[index]);
}
for (uint32_t index = 0; index < 16; ++index) {
matrix_b[index] = matrix_value(block, 1, index);
co_await load_matrix_value(context, 1, index, matrix_b[index]);
}
co_await FallingEdge{context.dut.clk};
context.dut.mat_load_valid.set(0);
context.dut.mat_start.set(1);
co_await RisingEdge{context.dut.clk};
co_await FallingEdge{context.dut.clk};
context.dut.mat_start.set(0);
for (uint32_t output = 0; output < 16; ++output) {
co_await RisingEdge{context.dut.clk};
co_await Delay{1_ps};
const uint32_t row = output / 4u;
const uint32_t column = output % 4u;
int64_t expected = 0;
for (uint32_t element = 0; element < 4; ++element) {
expected += static_cast(matrix_a[row * 4u + element]) *
matrix_b[element * 4u + column];
}
check(context, "matrix output index", context.dut.mat_out_index.get(),
output);
check(context, "matrix output data", context.dut.mat_out_data.get(),
static_cast(expected));
}
```
See [Performance](../performance.md#heavy-four-mode-comparison) for the latest
reference matrix and measurement cautions.