Resource Sharing
The previous article showed synthesis working from the Boolean function rather than the syntax. This article is about an optimisation that goes further and changes how many pieces of hardware exist.
Look at this RTL and count the adders:
always @(*) begin
if (mode)
result = a + b;
else
result = c + d;
end
The honest answer is that it depends on what the tool decides, not on how many + operators you typed. Because mode selects between two mutually exclusive branches, only one addition ever happens on a given cycle, so the tool is free to build a single adder with a mux in front of it selecting which operand pair to feed in, instead of two adders and a mux after them. This is resource sharing, and it is one of the more consequential optimisations a synthesis tool performs on its own.
Why the optimisation exists
An adder, especially a wide one, especially a pipelined one from Section 7.2, is real area. If the RTL never uses two adders’ worth of addition at the same time, which is exactly the case here since mode gates which one is active, building two adders and discarding one result through an output mux is waste: hardware that provably never does useful work at the same time as its twin.
Sharing moves the mux from after the operation (choosing between two computed results) to before it (choosing which operands feed the one operation). Same behaviour, less area, one adder fewer.
The cost: timing
Nothing is free. In the unshared version each adder’s critical path is only its own addition. In the shared version the path includes the input mux’s delay before the addition even starts; the shared adder cannot begin until the mux has resolved which operands it is working on. If that adder is on the critical path, the extra mux delay can be the difference between meeting and missing timing.
| Area | Timing (critical path through this logic) | |
|---|---|---|
| Unshared (2 adders) | Higher, pays for both adders | Lower, no extra mux ahead of the arithmetic |
| Shared (1 adder) | Lower, one adder plus input muxes | Higher, mux delay adds to the critical path |
This is a real trade, not an improvement in both directions, which is why the tool decides based on your constraints (Section 8.3) rather than applying it unconditionally.
When to prevent it
If a timing report (Section 8.3) shows the tool has shared a resource that sits on the critical path, you may need to discourage the sharing. Most tools have a directive for this (Design Compiler uses a set_resource_allocation style setting or a pragma, depending on version). The lever you control at the RTL level is often simpler: instantiate two named adder modules rather than writing two + operators in mutually exclusive branches. That removes the ambiguity and guarantees two independent adders exist.
// Explicit, unambiguous: two named adder instances, never merged by the tool
adder u_adder_mode1 (.a(a), .b(b), .sum(sum1));
adder u_adder_mode0 (.a(c), .b(d), .sum(sum2));
assign result = mode ? sum1 : sum2;
This gives up the area saving, but guarantees the timing you need when that adder is on a critical path and a shared version’s input mux would push you past the clock.
Resource sharing is one reason a synthesis report’s gate count and timing can look quite different from a line-by-line reading of the RTL, and why counting operators in the source is not a reliable area estimate. Real reports break area and timing down by source line or hierarchy so you can see where the tool applied optimisations like this and check they landed where you want them.
Practice Exercise
A design has two multiplies in mutually exclusive branches of a case, one of which is on the known critical path. Explain the trade you accept by letting the tool share one multiplier between them, versus instantiating two.
Solution
Sharing saves real area: a wide multiplier (recall from Section 7.2 how much internal structure even a moderate one needs) is expensive, and one instead of two saves the whole cost. But it adds an input mux in front of the shared multiplier’s already long critical path, which is the wrong place to add delay if that operation is already the bottleneck. Two separate instances avoid the extra delay, so each path is only its own multiply, at the cost of a second complete multiplier. Given one of these operations is known to be on the critical path, the safer default is to prevent sharing and pay the area, rather than risk missing timing to save area on the tightest path in the design.
Resource sharing lets the tool merge operations that never happen simultaneously into one shared unit with an input mux, trading area for the timing cost of that mux. It is usually a good trade, which is why tools apply it by default, and the wrong trade specifically when the shared unit is on the critical path, where the fix is a directive or, more portably, RTL that instantiates the operations as unambiguously separate hardware. In the next article we look at the general principle these two articles have been building toward: how to guide synthesis toward the structure you want without fighting it line by line.