C++ Part II · Module 27 · C++ for Semiconductor Verification
C++ or SystemVerilog: Deciding Where a Job Belongs
Every chapter in this module has assumed the split already decided: C++ computes, SystemVerilog drives pins. This chapter defends that split and, more usefully, says how to decide the cases it does not obviously cover. The decision has two inputs and most arguments only ever use one. The first is capability, which is categorical rather than a matter of degree: there are things SystemVerilog does that C++ cannot do at all, and the list is short enough to memorise. The second is throughput, and it is measured here rather than asserted — the same bitwise CRC over the same two hundred thousand bytes, once in behavioural Verilog under the simulator and once in compiled C++, with both arms first verified to produce the identical checksum. The gap is about five hundredfold, which is large enough to change architecture rather than just to be noted. It also comes with a caveat the measurement forces me to make honestly, because the simulator here is an interpreter and the compiling simulator that would narrow the gap is not installed, so the number describes this toolchain and not the language.
Advanced21 min readC++Part IIVerificationModeling
Part II · Module 27 · Chapter 27.10 · C++ or SystemVerilog: Deciding Where a Job Belongs
1. Two Inputs to the Decision
A job goes to one language or the other for one of two reasons, and keeping them apart makes most arguments short.
Capability. One language can express the requirement and the other cannot. This is categorical: no amount of effort moves the job.
Throughput. Both can express it, and one is dramatically faster. This is a measurement, and §3 takes it.
Almost every unproductive version of this argument is a capability question being discussed as if it were a preference, or a throughput question being settled by intuition. The useful order is: check capability first, because it is a short list and it decides most cases outright; measure throughput only for what is left.
2. The Capability List
These are the jobs where SystemVerilog is not merely better suited but is the only option.
| requirement | why C++ cannot | what to use |
|---|---|---|
| drive or sample a pin at a clock edge | no notion of time or of a signal | a driver with a clocking block |
| solve a constraint over related fields | no constraint solver | randomize() with constraint |
| a temporal property — "within three cycles" | no temporal operators | SVA, or PSL on nvc |
| deterministic concurrency between agents | std::thread is preemptive — all of Module 26 | fork/join |
| represent X or Z | C++ has two values per bit | 4-state SystemVerilog types |
| functional coverage with bins and crosses | no language support | covergroup |
Two of those rows deserve a note.
Concurrency is the one people try hardest to move, and Module 26 is the nine-chapter argument against it: std::thread gives you data races, deadlock, memory ordering and a debugging problem whose symptoms do not reproduce, in exchange for concurrency you did not need — because a testbench's agents do not need to run on separate cores, they need to interleave deterministically, which is exactly what fork/join provides and what threads do not.
And the X/Z row is the one that silently breaks comparisons. Chapter 27.1 §6 states it and the C track's Module 25 measured it: an X read through an integer-valued interface becomes a definite number, so a DUT driving X and a model computing 0 agree. That is not a modelling shortcoming to be worked around; it is a reason the check belongs in SystemVerilog.
The mirror list, where C++ is the only sensible option, is shorter and softer — most of these can be done in SystemVerilog, badly: a reference model of any complexity, parsing and post-processing, reuse of the same code by firmware, and anything that must run without a simulator licence.
3. The Throughput Measurement
For jobs both languages can express, the question is how much faster. The workload is a bitwise CRC-8 — eight iterations of shift-and-conditional-xor per byte — which is a fair stand-in for the per-item arithmetic a reference model does.
The control comes first. A speed comparison between two programs that compute different things is worthless, so both arms print their checksum:
verilog N=20000 crc=0xcf
verilog N=200000 crc=0xd8
c++ N=20000 crc=0xcf
c++ N=200000 crc=0xd8Identical at both sizes. Now the timing means something.
The simulator arm is timed at two sizes so that process start-up cancels out of the slope — the trap Chapter 26.9 §5 measured, where a baseline consisting mostly of start-up produced a meaningless 111× ratio:
| arm | 20,000 bytes | 200,000 bytes | per-byte slope |
|---|---|---|---|
vvp (interpreted Verilog) | 123.16 ms | 1022.71 ms | ≈ 4,998 ns/byte |
With the run-to-run ranges, the slope is bounded between 4,344 and 6,092 ns/byte.
The C++ arm is timed in-process, minimum of 31 repetitions, because its per-byte cost is far too small for process timing to resolve:
c++ in-process N=200000 crc(off=0)=0xd8 … ns/byteAnd here the measurement did something worth reporting rather than smoothing over. Two series of the same binary on the same machine did not overlap: nine runs gave a median of 7.99 ns/byte with a range of 7.99–8.40, and fifteen runs later gave a median of 10.36 with a range of 9.58–11.56. Load average was 7 to 8 both times. The likely explanation is core scheduling — this is an Apple M3 with four performance and four efficiency cores, as Chapter 26.1 §1 established, and a short-lived process may land on either.
So the honest C++ figure is 8 to 12 ns/byte, and the ratio has to be published as a range:
| value | |
|---|---|
vvp per byte | ≈ 4,998 ns [4,344–6,092] |
| C++ per byte | ≈ 10 ns [7.99–11.56] |
| ratio | ≈ 500×, conservatively bounded 376× to 636× |
An earlier measurement session gave 691×. Three estimates spanning roughly 400 to 700 agree on the only thing that matters architecturally: the gap is between two and three orders of magnitude, and quoting it as "about 500×" with that range is the defensible form.
4. The Caveat That the Measurement Forces
Icarus Verilog is an interpreter. vvp walks a compiled-to-bytecode representation of the design, evaluating statements one at a time. A compiling simulator — Verilator, or any of the commercial tools — translates the design into C++ or machine code and runs it natively, and for purely behavioural arithmetic like this it should be far closer to the C++ arm.
Verilator is not installed on this machine, so I cannot measure it, and I will not estimate it. What that means for the claim:
The measured statement is narrow: on this toolchain, the same algorithm is roughly 500× faster in compiled C++ than under vvp. That is a true and useful fact about a very widely used open-source simulator.
The statement I am not making is "SystemVerilog is 500× slower than C++". That would be a claim about the language, derived from one implementation, and it is exactly the inference Chapter 26.8 §9 refused to make about x86 memory ordering from arm64 measurements.
And the engineering conclusion survives the caveat, which is why the chapter is still worth its measurement. Even if a compiling simulator closed the gap to 10×, the shape of the decision does not change: a model doing per-item arithmetic over millions of items belongs in compiled code, and the right move when you do not know your own simulator's factor is to measure it — the two-size slope method in §3 takes about ten minutes and the answer is specific to your tools.
5. The Division That Falls Out
Applying both criteria to a conventional environment gives the split this module has been assuming:
| component | language | because |
|---|---|---|
| driver, monitor | SystemVerilog | pins and clock edges — capability |
| sequences, scenarios | SystemVerilog | constrained randomisation — capability |
| temporal assertions | SVA | temporal operators — capability |
| functional coverage | SystemVerilog | covergroup — capability |
| reference / golden model | C++ | complexity, reuse, throughput |
| scoreboard comparison | either | C++ if the model is there already |
| register and memory models | C++ | data structures, and firmware reuse |
| frame encode/decode | C++ | bit manipulation, and firmware reuse |
| log and coverage post-processing | C++ or a script | no simulator needed at all |
Note how few rows are decided by speed. Four are capability, and the C++ rows are mostly decided by complexity and reuse. That is the realistic picture, and it is why §4's caveat does not damage the chapter's conclusion.
Two rows worth a sentence. The scoreboard can go either way, and the deciding factor is usually where the model already is — splitting the model and the comparison across the language boundary adds a translation layer for no benefit. And post-processing should default to running outside the simulation entirely, which is Chapter 27.1 §4's third mechanism: no glue, unit-testable, and no licence consumed in CI.
6. The Cost of the Boundary
Every split has a seam, and the seam has costs that an enthusiastic "move it to C++" decision tends to omit.
Translation code. Each crossing needs argument marshalling. Chapter 27.1 §4's VPI layer is the entire cost of its model — the model proper never mentions VPI — and that is the shape to aim for, but it is still code to write and test.
Per-call overhead. A boundary crossing is not free, and the pattern that defeats the whole exercise is calling across it per bit or per cycle rather than per transaction. If the C++ side is 500× faster per item but you cross the boundary a thousand times more often, you have bought nothing.
Debuggability. A stack that spans two languages is harder to debug than either alone: the simulator's waveform does not show C++ state, and a C++ debugger does not show simulation time. Chapter 26.9 §7's advice applies — know which half has stopped before investigating.
Build complexity. Chapter 27.1 §5's two-step build exists because the wrapper compiles at C++11. Every environment acquires a few of these, and they are maintenance.
And type mismatches at the seam. Four-state to two-state, bit widths that are not a multiple of eight, signedness. Each is a place the two sides can disagree while both appear correct.
The rule that follows: cross the boundary at the coarsest granularity the problem allows — per transaction or per frame, never per cycle — and keep the model itself free of the glue so it stays testable on its own.
7. Three Cases Worth Deciding Explicitly
A checksum or CRC over a large payload. C++, decisively. It is pure arithmetic over many items, it is reused by firmware, and §3's factor applies directly.
A scoreboard for a protocol with out-of-order completions. Either, and the real question is Chapter 27.6 §5's: the matching logic needs a map keyed by transaction id, which is more pleasant in C++, while the monitor feeding it must be SystemVerilog. Put the comparison wherever the model is.
An end-to-end "did the design respond within N cycles" check. SystemVerilog, categorically — it is a temporal property, and the model has no clock. Attempting it in C++ means reconstructing time from timestamps the monitor passes over, which is both more code and less expressive than one SVA property.
The pattern in all three: state the requirement in one sentence and look for the words that decide it. "Within N cycles" means SystemVerilog. "Over a million items" means measure. "Also needed by firmware" means C++. "Must constrain these five fields against each other" means SystemVerilog.
8. The Table
| the question | the answer | measured |
|---|---|---|
| the two inputs to the decision | capability then throughput | §1 |
| which decides most cases | capability — it is categorical | §4 callout |
| what can C++ not do at all | pins, constraints, temporal properties, X/Z, deterministic concurrency, coverage | §2 |
| did the two arms compute the same thing | yes — 0xcf and 0xd8 at both sizes | §3 |
vvp per byte | ≈ 4,998 ns [4,344–6,092] | §3 |
| C++ per byte | ≈ 10 ns — two series, [7.99–11.56] | §3 |
| the ratio | ≈ 500×, bounded 376–636× | §3 |
| why is the C++ figure a range | two non-overlapping series, probably P-core/E-core | §3 |
| what is the claim not | that SystemVerilog is 500× slower — vvp is an interpreter | §4 |
| was a compiling simulator measured | no — Verilator is absent here | §4 |
| when does throughput decide architecture | when it forces you to run fewer items | §4 callout |
| what makes a speed comparison valid | a control and a slope | §4 callout |
| at what granularity should you cross | per transaction or frame, never per cycle | §6 |
9. What to Actually Do
- Check capability first. If the requirement mentions a clock, a constraint between fields, a temporal window, X/Z or coverage bins, it is SystemVerilog and the discussion is over.
- Measure throughput on your own tools with the two-size slope method — it takes ten minutes and the answer is specific to your simulator.
- Make both arms print the same answer before timing them. A speed comparison between different computations is void.
- Time the fast arm in-process, minimum of N repetitions, and defeat the optimiser with a varying input and a barrier.
- Report a range when your series disagree, and say what you think caused it.
- Do not generalise an interpreter's factor to a language.
- Let throughput decide only when it changes the item count you can afford — that is when it becomes a coverage question.
- Cross the boundary per transaction or per frame, never per cycle.
- Keep the model free of simulator types so it stays testable without a simulation.
- Put the scoreboard wherever the model already is, and default post-processing to outside the simulator entirely.
10. Mistakes Worth Making Once
Arguing throughput when the question is capability. Three sentences of measurement for a decision that was categorical.
Moving pin driving into C++. You have reimplemented a clocking block without the sample-versus-drive semantics.
Modelling concurrent agents with std::thread. Module 26 is the bill.
Comparing a value that may be X in C++. The X becomes a number and the comparison passes.
Timing two programs that compute different things. The number means nothing.
Timing the fast arm by process wall-clock. Start-up dominates — Chapter 26.9 §5's 111×.
Timing a pure loop without a barrier. It measured 0.00 ns/byte, because the optimiser hoisted it.
Publishing the series that agreed with you. Two series here did not overlap, and both are in §3.
Quoting "SystemVerilog is 500× slower". That is an interpreter's number wearing a language's name.
Crossing the boundary per cycle. The C++ side is faster per item and you have made a thousand times more calls.
Splitting the model and the scoreboard across the seam. A translation layer for no benefit.
11. Interview Relevance
The frozen curriculum marks this Critical, and it is a judgement question rather than a knowledge one: what would you write in C++ and what in SystemVerilog?
Answer with the two criteria rather than a list. Capability first, because it is categorical: pins and clock edges, constraint solving over related fields, temporal assertions, X/Z representation, deterministic concurrency between agents, and functional coverage are all things SystemVerilog does and C++ cannot, so those decide themselves. Then throughput for whatever remains.
Then the measurement, with its control. The same bitwise CRC over 200,000 bytes ran at roughly 5 µs per byte under the Icarus Verilog interpreter and about 10 ns per byte in compiled C++ — about 500× — and both arms were first verified to produce the identical checksum, with the simulator timed at two sizes so process start-up cancels out of the slope.
Then the caveat, unprompted, because it is what makes the rest credible. Icarus is an interpreter; a compiling simulator like Verilator would narrow the gap substantially, and it was not available to measure, so the claim is about that toolchain rather than about SystemVerilog as a language.
And then the senior point, which is that the speed number should rarely decide anything: a model called ten thousand times in a run does not care about 500×, and the factor only becomes architectural when it forces you to run fewer items — at which point it has turned into a coverage question. Finishing with the boundary rule — cross per transaction, never per cycle, or the per-call overhead eats the gain — shows you have integrated one rather than read about it.
12. What You Should Be Able to Say Now
- That the decision has two inputs, capability then throughput, and that capability is categorical and settles most cases.
- The capability list: pins and clock edges, constrained randomisation, temporal assertions, X/Z, deterministic concurrency, functional coverage.
- That both arms of the speed comparison computed the same checksum (0xcf and 0xd8) before any timing was taken.
- That
vvp's per-byte slope was ≈ 4,998 ns [4,344–6,092], measured at two sizes so start-up cancels. - That C++ measured ≈ 10 ns/byte, from two non-overlapping series (7.99 [7.99–8.40] and 10.36 [9.58–11.56]) probably reflecting P-core/E-core scheduling — so the figure is a range.
- That the ratio is therefore about 500×, bounded 376–636×, with an earlier session giving 691×.
- That Icarus is an interpreter, that Verilator is absent here, and that the claim is therefore about this toolchain and not about SystemVerilog.
- That a throughput factor matters only when it forces a smaller item count — at which point it is a coverage argument.
- That a valid speed comparison needs a control and a slope, and that the fast arm must be timed in-process with a barrier (an unbarriered loop measured 0.00 ns/byte).
- That the boundary should be crossed per transaction or frame, never per cycle, and that the model should stay free of simulator types.
13. Exercises
Exercise 1 — the capability list. For five checks in an environment you know, classify each as capability-decided or throughput-decided. Count how many needed a measurement.
Exercise 2 — the control. Write the same algorithm in behavioural Verilog and in C++ and confirm they produce the same answer at two sizes before timing anything.
Exercise 3 — the slope. Time the simulator arm at two sizes and compute the per-byte slope. Then time it at one size and see how much start-up would have contributed.
Exercise 4 — the barrier. Time the C++ arm in-process without a barrier and observe 0.00 ns/byte. Add a varying input and a barrier and get a real number.
Exercise 5 — two series. Run your in-process timing twice, twenty runs each, on different occasions. Report both ranges, not the one you prefer.
Exercise 6 — your own simulator. Repeat the whole measurement on the simulator your project actually uses and write down the factor. It is the only one that matters to you.
Exercise 7 — the boundary cost. Call a C++ model once per transaction, then once per byte, and measure both. Find the granularity at which the gain disappears.
14. Summary
- Decide on capability first, throughput second. Capability is categorical and settles most cases: pins and clock edges, constraint solving, temporal assertions, X/Z, deterministic concurrency and functional coverage are SystemVerilog's and C++ cannot do them at all.
- Measured, with a control: the same bitwise CRC produced the identical checksum in both languages (0xcf at 20,000 bytes and 0xd8 at 200,000) before any timing was taken.
vvpcost ≈ 4,998 ns/byte [4,344–6,092], measured at two sizes so process start-up cancels out of the slope.- C++ cost ≈ 10 ns/byte, from two series that did not overlap — 7.99 [7.99–8.40] and 10.36 [9.58–11.56] — most likely performance-core against efficiency-core scheduling, so the figure is reported as a range.
- The ratio is about 500×, conservatively bounded 376–636×, with an earlier session giving 691×. The agreement that matters is on the order of magnitude.
- The claim is about this toolchain, not the language. Icarus Verilog is an interpreter and Verilator is absent here, so a compiling simulator would narrow the gap by an amount I did not measure and will not estimate.
- Throughput should decide only when it forces a smaller item count, because that is when it stops being a speed question and becomes a coverage question.
- A valid speed comparison needs a control and a slope, the fast arm timed in-process with a barrier — an unbarriered pure loop measured 0.00 ns/byte because the optimiser hoisted it.
- The division that falls out puts drivers, monitors, sequences, assertions and coverage in SystemVerilog, and models, register and memory state, frame encoding and post-processing in C++ — with only a minority of those rows decided by speed at all.
- Cross the boundary at the coarsest granularity available — per transaction or frame, never per cycle — and keep the model free of simulator types so it remains testable without a simulation.
Chapter 27.11 assembles the module into one worked flow, and validates the model against an outside authority before letting it judge anything.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
