Chip companies verify their designs by running huge farms of slow register-transfer-level simulations in parallel, but the simulations quietly choke each other — a big design's simulator is a giant program that overflows the shared cache, so 48 cores deliver only about 12 times the throughput of one; this work from UC Santa Cruz notices that modern chips are full of repeated blocks like identical CPU cores, and makes the simulator emit one shared copy of a repeated module's code instead of many, shrinking its cache footprint for up to 1.95 times faster single runs and 2.09 times more overall throughput.
The argument in one glance
-
Problem
Running many RTL simulations in parallel does not scale
Designing a chip requires vast numbers of register-transfer-level (RTL) simulations for design, debugging, and verification, and they are slow, so companies run many at once on server farms. But the parallel runs choke each other: a fast full-cycle simulator compiles the whole design into one huge program, and when many such programs run together they need far more shared last-level cache than the machine has, so cores stall on memory. On a large design, 48 cores running 48 simulations reach only about 12 times the throughput of a single one.
-
What people did
Throw more cores, more cache, or dedup small statements at it
The usual responses are to buy bigger machines with more cores and larger caches (AMD even stacks extra cache with 3D V-Cache), and, in the simulator itself, to deduplicate tiny repeated pieces of code — Verilator, the popular open-source simulator, shares code for small SystemVerilog statements.
-
Why it fell short
The cache stays the wall, and small-scale dedup barely helps
More cores just add more simulations competing for the same cache, so throughput plateaus; even huge stacked caches only partly relieve the pressure on a big design. And deduplicating only small statements leaves almost all the redundancy on the table — a simulator for an eight-core chip still carries eight nearly identical copies of the core's code. The real opportunity, deduplicating whole large repeated modules, is hard: naive sharing creates scheduling deadlocks and a performance tax that can make things slower.
-
This paper's idea
Emit one shared copy of a repeated module, scheduled for locality
This work deduplicates at a coarse grain: it finds a module instantiated many times (like the CPU cores), generates one shared copy of its code that all instances reuse, and carefully partitions the design so that sharing never creates a cycle. It then schedules the reused copies back-to-back so the shared code stays hot in the instruction cache. The shrunken footprint relieves the cache bottleneck for up to 1.95 times faster single runs and 2.09 times more parallel throughput.
Why this problem is worth solving
The reason more hardware does not fix the problem is that the bottleneck is the shared cache, and adding simulations only makes the contention worse. RTL simulation is instruction-bound, not arithmetic-bound. The fastest simulators are full-cycle: rather than reacting to individual signal changes like an event-driven interpreter, they compile the entire hardware design ahead of time into one big straight-line program that unconditionally evaluates the whole circuit every cycle. That removes scheduling overhead and is fast, but the program grows with the design, and a large design produces a simulator binary so big it overwhelms the processor’s frontend — its instruction cache and branch predictor. A single such simulation is already limited by how fast the machine can fetch its own code. Now run many of them at once, as a verification farm does. Each needs a large slice of the last-level cache, and together they demand far more than the machine physically has, so they spill to main memory and cores sit idle waiting on it. This is why a batch does not scale linearly: on a large design, 48 parallel simulations reach only about 12 times the throughput of one, and cramming in more simulations past that point actually lengthens each one. The path to more throughput is not more cores but a smaller cache footprint per simulation.
The way to shrink that footprint is to stop storing the same code many times, but deduplication in this setting is deceptively hard. Modern chips are full of replication — many identical cores, tiles, vector lanes, reused third-party blocks — and a simulator without deduplication emits a separate copy of the code for every instance, even though they are identical. The obvious fix, generate the code once and share it, runs into two walls. First, cycles. A circuit is a graph, and a full-cycle simulator schedules it by breaking it into partitions and topologically sorting them so each is evaluated once per cycle, which requires the partition graph to stay acyclic. But a module has bidirectional connections to its surroundings, and different instances of the same module sit in different contexts, so a partitioning that is acyclic for one instance can form a cycle when reused for another — and a cycle in the partition graph is a scheduling deadlock, breakable only by re-evaluating pieces repeatedly, which is ruinously slow. Second, the dedup tax. Shared code cannot hardcode where each instance’s signals live; it must reach them through pointers. That memory indirection blocks compiler optimizations that otherwise keep signals in registers, and it makes the code execute more instructions. So deduplication only pays if enough of the design is shared to overcome the tax it imposes.
What people did before, and where it broke
- Buy more cores. Run more parallel simulations on bigger machines. Where it broke. Every added simulation competes for the same last-level cache, so throughput plateaus and each individual run gets slower; past an inflection point, more parallelism is counterproductive.
- Buy more cache (for example 3D V-Cache). Stack extra last-level cache onto the CPU to hold more of the working set. Where it broke. RTL simulation is so cache-hungry that even a large stacked cache only partly relieves the pressure on big designs, where simulations still end up competing for cache.
- Fine-grained deduplication (Verilator). Share code for small repeated statements. Where it broke. It only handles simple statement types and captures a tiny fraction of the redundancy; a simulator for an eight-core chip still holds eight nearly identical copies of the whole core, and in practice its dedup barely moves performance.
- Naive module-level sharing. Turn a whole repeated module into a single shared function. Where it broke. Different instances have different surrounding contexts, so a shared partitioning introduces cycles into the schedule graph, causing deadlocks that can only be broken by expensive repeated evaluation.
- The gap. No simulator captured the big win — deduplicating whole large repeated modules — while keeping the schedule acyclic and overcoming the indirection tax. That requires a partitioning that stays acyclic across every instance and a schedule that makes the shared code cheap to run, which is exactly what this work builds.
How coarse-grained deduplication works
From eight copies of a core to one shared, locality-scheduled copy
-
-
new
-
new
-
new
-
new
-
new
The idea
Reuse one copy of the code for every identical instance, and concentrate on the module that gives the most. A modern design is not a pile of unique parts; it replicates a few big building blocks, so the tool finds the module whose instances times size dominates the footprint — the cores — and shares a single copy of its code across all of them, letting the long tail of unique logic keep its own code. Pouring the optimization into the small set of components that account for almost all the redundancy, rather than spreading it thin, is the same instinct the low-bit language-model serving work covered elsewhere on this site follows, spending its effort on the concentrated subset that dominates the cost and handling everything else the ordinary way.
Deduplicate only when there is enough replication to beat the tax. Sharing code is not free — it forces every shared signal through a pointer, blocking optimizations and running more instructions, a cost the authors name the dedup tax. On a design with little repetition the tax dominates and deduplication is a net loss; only when a module is replicated enough does the shrunken footprint pay it back with interest. Applying a structural transformation just where it earns its keep, and declining it where it would only add overhead, is exactly the discipline the page-table replication work covered elsewhere on this site adopts, duplicating structures only when the access pattern shows the copy will pay for itself rather than paying everywhere by default.
Arrange the reused code so it stays resident in the fast cache. Deduplication does not only shrink the binary; because the same shared code now runs once per instance every cycle, scheduling those runs close together keeps it hot in the instruction cache and branch predictor, turning repetition into locality. The whole benefit hinges on this reorganization, since instruction-cache misses cannot be hidden by out-of-order execution. Reshaping the generated code and its schedule to fit how the processor actually fetches and predicts, rather than leaving locality to chance, is the same move the recommendation-model compiler work covered elsewhere on this site makes, restructuring a workload around the hardware’s real execution behavior so the fast path stays fed.
Following an eight-core design through deduplication
Sharing one core's code across eight instances
Tracing an eight-core design shows the partitioning and scheduling working together.
Step 1 of 5
Identify the repeated module
The design instantiates eight identical CPU cores. Scanning the module hierarchy, the tool picks the core module to deduplicate, because its instance count times its size is the largest of any repeated module — the biggest slice of the footprint.
Interconnect and peripherals are singletons and are left alone.
Why it works
- A full-cycle binary grows with the design. The simulator inlines the whole circuit into straight-line code → so eight identical cores mean eight copies of the core’s code overflowing the cache → so emitting one shared copy shrinks the footprint and relieves the contention that caps parallel throughput.
- Cycles live only on the boundary. A cycle purely inside an acyclically-partitioned instance is impossible, so any cycle from reuse must cross the instance boundary → so dissolving just the boundary partitions removes every cycle in practice → so the interior stays identical across instances and its code can be shared.
- Repetition becomes locality. The shared code runs once per instance every cycle → so scheduling those runs back-to-back keeps it resident in the instruction cache and branch predictor → so instruction-cache misses, which out-of-order execution cannot hide, largely disappear.
- The frontend win outweighs the tax. Indirection makes deduplicated code execute about 12 percent more instructions → but fitting far more code in the L1 instruction cache raises instructions-per-cycle by around 80 percent → so deduplication is a net win once replication is high enough to earn back the tax.
- Partitioning is done once, then reused. Only a single instance of the repeated module needs partitioning → so the simulator’s graph-partitioning step does a small fraction of the work → so build time drops to under a fifth on a large replicated design, a bonus on top of the runtime speedup.
What it costs (trade-offs and scope)
- No benefit without enough replication, and a slowdown with a little. Single-core or low-reuse designs gain essentially nothing, and dual-core designs are actually slower, because the dedup tax is not yet overcome; substantial replication (many cores) is required before deduplication pays off.
- The dedup tax is real. Reused code must reach its data through pointers instead of hardcoded addresses, blocking compiler optimizations and executing about 12 percent more instructions; the entire gain depends on better instruction-cache behavior outweighing that.
- Coverage falls short of the ideal. Dissolving boundary partitions to stay acyclic means the peripheral code cannot be reused, so the real node reduction trails the theoretical maximum — for example about 64 percent versus an ideal 89 percent on an eight-core design.
- It deduplicates only one module. The prototype picks the single highest-benefit module and its direct instances; deduplicating multiple modules or nesting the technique is left as future work, and interconnect and peripherals are not deduplicated at all.
- It is tied to full-cycle simulation and acyclic designs. The work is built on and measured within the ESSENT simulator (FIRRTL in, C++ out), assumes identical instances exist in the module hierarchy and that the design is mostly acyclic, and the specific numbers are ESSENT-based; event-driven simulators do not benefit the same way.
- Locality scheduling has limits. Real data dependencies mean not every corresponding partition can be placed adjacently, so the reuse distance cannot always be fully minimized.
The evaluation
Three levels of deduplication in an RTL simulator
| Property | No dedup (ESSENT baseline) | Fine-grained (Verilator) | Coarse-grained (this work) |
|---|---|---|---|
| What gets shared | |||
| Cache footprint on a many-core design | Large; one copy per core | Barely reduced | |
| Single-run speed with high reuse | Baseline | About the same as no dedup | |
| Parallel throughput per server | Plateaus early on cache contention | About the same |
Check yourself
Check your understanding
1. Why does running many independent RTL simulations in parallel fail to scale linearly?
2. What are the two main obstacles to deduplicating whole repeated modules, and how does the paper handle the cycle problem?
3. How does the paper turn deduplication into a performance win despite the dedup tax?
4. Which statement best reflects the results and scope of this work?
Sources
The paper is “Don’t Repeat Yourself! Coarse-Grained Circuit Deduplication to Accelerate RTL Simulation” by Haoyuan Wang, Thomas Nijssen, and Scott Beamer, published at ASPLOS 2024, DOI 10.1145/3622781.3674184. The authors are at the University of California, Santa Cruz; the paper is open access (CC-BY) and the work is built on the open-source ESSENT simulator.
This explainer is written from the full text (16-page eScholarship version). Everything above comes from the paper’s own sections: the abstract and introduction identifying the shared last-level cache as the throughput bottleneck (the sub-linear parallel scaling reaching about 12 times on 48 cores); the background on event-driven versus full-cycle simulation, the cache-hungriness of RTL simulation, Verilator and ESSENT, and graph representation and acyclic partitioning; the motivation and three challenges (avoiding cycles, the dedup tax from indirect memory accesses, and data layout) plus the code-locality opportunity; the partitioning-for-deduplication method (choosing the maximum-benefit module, templating one instance, dissolving boundary partitions to break cycles, and re-partitioning the rest); the data layout with per-instance structs and the consolidate-sort-disassemble locality-aware scheduling; and the evaluation on Rocket and Boom RISC-V designs across Intel Xeon and AMD 3D-V-Cache platforms — the up-to-1.95-times single-simulation speedup, the up-to-2.09-times throughput, the dual-core slowdown from the dedup tax, the node-reduction figures (ideal versus real), the performance-counter analysis (about 12 percent more instructions but roughly 81 percent higher instructions-per-cycle, lower L1I and branch misses), and the partitioning-time reduction to about 17.6 percent of the baseline.