Synthesizable Combinational Logic & Arithmetic Datapaths
Volume 01 was about writing Verilog that means what you think it means. This volume is about architecture: two circuits can compute the same function and differ by an order of magnitude in speed. Every structure here exists because someone refused to accept a delay that grew linearly with width.
2.1 Multiplexers, decoders & priority encoders
The multiplexer is the most important primitive in digital design. Not because it is
clever, but because everything becomes one: an if statement, a
case statement, a ternary operator, a register with an enable. Learn to see
MUXes in your RTL and you can estimate area before you ever run synthesis.
Three ways to write the same MUX
All three of these synthesize to identical hardware. Pick based on readability, not on some belief that one is "more efficient".
// ---- Style A: indexed part-select. Scales to any N without editing. ------
module mux_n #(
parameter W = 8, // width of each channel
parameter N = 4 // number of channels
) (
input wire [N*W-1:0] din, // flattened: { ch3, ch2, ch1, ch0 }
input wire [$clog2(N)-1:0] sel,
output wire [W-1:0] dout
);
// "Starting at bit sel*W, take W bits going up." Synthesizes to a MUX.
assign dout = din[sel*W +: W];
endmodule
// ---- Style B: explicit case. Clearest when channels have real names. ----
module mux4_case #(
parameter W = 8
) (
input wire [W-1:0] d0, d1, d2, d3,
input wire [1:0] sel,
output reg [W-1:0] y
);
always @(*) begin
case (sel)
2'd0: y = d0;
2'd1: y = d1;
2'd2: y = d2;
2'd3: y = d3;
// sel is 2 bits, so all four values are already covered. The default
// is unreachable - assigning X tells synthesis "I do not care",
// which lets it optimise. It also guarantees no latch (Volume 1.5).
default: y = {W{1'bx}};
endcase
end
endmodule
// ---- Style C: structural tree, built from 2:1 primitives ----------------
module mux2 #(
parameter W = 8
) (
input wire [W-1:0] a, b,
input wire s,
output wire [W-1:0] y
);
assign y = s ? b : a;
endmodule
module mux4_tree #(
parameter W = 8
) (
input wire [W-1:0] d0, d1, d2, d3,
input wire [1:0] sel,
output wire [W-1:0] y
);
wire [W-1:0] lo, hi;
// Both first-level MUXes evaluate CONCURRENTLY - 2 levels deep, not 3.
mux2 #(W) u_lo (.a(d0), .b(d1), .s(sel[0]), .y(lo));
mux2 #(W) u_hi (.a(d2), .b(d3), .s(sel[0]), .y(hi));
mux2 #(W) u_out (.a(lo), .b(hi), .s(sel[1]), .y(y));
endmodule
Tree vs cascade: same gate count, different depth
Here is where architecture starts to matter. A 4:1 MUX built from three 2:1 MUXes uses three MUXes either way - but wiring them as a chain makes the path three levels deep, while wiring them as a tree makes it two. At 4:1 that is a rounding error. At 64:1 it is 63 levels versus 6.
d0 must physically pass through
every MUX. Scale to 64:1 and the gap becomes 6 levels versus 63.
if/else if
chain), you have specified that earlier conditions win, and that ordering constraint is
real logic the tool must preserve. Priority costs depth. If your conditions are mutually
exclusive, use case and say so.
Priority encoders
A priority encoder answers "which is the highest-numbered active request?" - the core of
every interrupt controller and bus arbiter. The casez construct expresses it
almost literally, with ? meaning "don't care".
module priority_encoder_8to3 (
input wire [7:0] req,
output reg [2:0] grant,
output wire valid // 0 => no request, grant is meaningless
);
assign valid = |req; // OR-reduction: any bit set?
always @(*) begin
// casez treats ? in the PATTERN as don't-care. The first matching
// branch wins, which is exactly the priority semantics we want.
casez (req)
8'b1???????: grant = 3'd7; // bit 7 highest priority
8'b01??????: grant = 3'd6;
8'b001?????: grant = 3'd5;
8'b0001????: grant = 3'd4;
8'b00001???: grant = 3'd3;
8'b000001??: grant = 3'd2;
8'b0000001?: grant = 3'd1;
8'b00000001: grant = 3'd0;
default: grant = 3'd0; // req == 0; `valid` tells you to ignore it
endcase
end
endmodule
casez, never casex
casex treats X in the input as a wildcard too. One
uninitialised bit arriving at a casex can match a branch it has no business
matching, and the mismatch between simulation and synthesized hardware is brutal to
debug. casez only wildcards Z, which effectively never appears
in internal logic. Industry style guides ban casex outright.
Interview grilling - "Build a 4:1 MUX from 2:1 MUXes. Then build a 2:1 MUX from NAND gates only."
Part 1 - 4:1 from 2:1. Three MUXes in a tree, as in Figure 2.1:
two select between {d0,d1} and {d2,d3} using
sel[0], and the third picks between those results using
sel[1]. Say "two levels deep" out loud - that is the half they are
actually testing.
Part 2 - 2:1 from NAND. Start from the Boolean expression:
Apply De Morgan to turn the OR of two ANDs into a NAND of two NANDs:
And ~S = NAND(S, S). So the answer is four NAND gates:
one to invert S, two for the product terms, one to combine them.
module mux2_nand (input wire a, b, s, output wire y);
wire s_n, n1, n2;
nand (s_n, s, s); // inverter built from a NAND
nand (n1, a, s_n);
nand (n2, b, s);
nand (y, n1, n2);
endmodule
The follow-up: "Why does anyone care about NAND-only?" Because NAND is functionally complete and, in static CMOS, cheaper than AND or OR - an AND gate is physically a NAND followed by an inverter. Standard cell libraries are NAND/NOR heavy for exactly this reason.
2.2 Adders, subtractors & carry propagation networks
Addition is the hardest cheap thing in digital design. The logic per bit is trivial; the problem is that bit i cannot finish until it knows the carry from bit i-1. That dependency is the whole subject.
The full adder
Cout = (A · B) + (Cin · (A ⊕ B))
The Cout form above deliberately reuses A ⊕ B, which the sum
already needs - sharing that term is why the standard cell version is compact.
module full_adder (
input wire a, b, cin,
output wire sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (cin & (a ^ b));
endmodule
module ripple_carry_adder #(
parameter W = 8
) (
input wire [W-1:0] a, b,
input wire cin,
output wire [W-1:0] sum,
output wire cout
);
wire [W:0] c;
assign c[0] = cin;
genvar i;
generate
for (i = 0; i < W; i = i + 1) begin : g_bit
full_adder u_fa (
.a (a[i]),
.b (b[i]),
.cin (c[i]), // <-- THE PROBLEM: stage i waits for stage i-1
.sum (sum[i]),
.cout (c[i+1])
);
end
endgenerate
assign cout = c[W];
endmodule
Simple, tiny, and slow. The critical path runs from a[0] all the way to
cout, through every stage. Delay is O(N). A 64-bit ripple
carry adder is roughly 128 gate delays - utterly unusable at any serious clock frequency.
Carry lookahead: computing all carries at once
The insight is that each bit position can decide two things without knowing its incoming carry:
Pi = Ai ⊕ Bi (this bit propagates an incoming carry)
Every G and P depends only on the inputs, so all of them settle after
one gate delay, simultaneously. Now the carry recurrence
Ci+1 = Gi + Pi·Ci can be
unrolled into flat expressions with no chaining at all:
C2 = G1 + P1G0 + P1P0C0
C3 = G2 + P2G1 + P2P1G0 + P2P1P0C0
C4 = G3 + P3G2 + P3P2G1 + P3P2P1G0 + P3P2P1P0C0
module cla_4bit (
input wire [3:0] a, b,
input wire cin,
output wire [3:0] sum,
output wire cout
);
wire [3:0] g = a & b; // generate: this bit makes a carry on its own
wire [3:0] p = a ^ b; // propagate: this bit passes a carry through
wire [4:0] c;
assign c[0] = cin;
// Each carry is a FLAT expression of g, p and cin - no chaining.
// Every one of these four lines evaluates at the same time.
assign c[1] = g[0] | (p[0] & c[0]);
assign c[2] = g[1] | (p[1] & g[0])
| (p[1] & p[0] & c[0]);
assign c[3] = g[2] | (p[2] & g[1])
| (p[2] & p[1] & g[0])
| (p[2] & p[1] & p[0] & c[0]);
assign c[4] = g[3] | (p[3] & g[2])
| (p[3] & p[2] & g[1])
| (p[3] & p[2] & p[1] & g[0])
| (p[3] & p[2] & p[1] & p[0] & c[0]);
// Once the carries exist, the sums are one XOR each.
assign sum = p ^ c[3:0];
assign cout = c[4];
endmodule
c[4]: five product terms, the widest with five inputs. Extend the
pattern to bit 63 and you need a 64-input AND gate, and p[0] must drive 64
separate loads. Both are physically impossible. Real adders build CLA in
blocks - 4-bit groups with their own group-generate and group-propagate,
then a second level of lookahead across the groups. That hierarchy is what actually
delivers the log-depth.
The adder family in practice
| Architecture | Delay | Area | Where it is used |
|---|---|---|---|
| Ripple Carry | O(N) | Smallest | Narrow adders, non-critical paths, counters |
| Carry Select | O(√N) | ~2× | Computes both carry-in cases, MUXes the winner |
| Carry Lookahead | O(log N) | Large | The classic textbook fast adder |
| Kogge-Stone | O(log N), minimum depth | Largest - heavy wiring | High-frequency CPU datapaths |
| Brent-Kung | O(log N), ~2× depth of K-S | Much smaller than K-S | Area- and power-sensitive designs |
assign sum = a + b;. Modern synthesis tools have every one of these
architectures in their arithmetic library and will pick one based on your timing
constraint - ripple if the path is slack, Kogge-Stone if you are pushing the clock. Hand
instantiating a CLA is almost always a mistake. Know the structures so you can
read timing reports and explain them in interviews, not so you can type them out.
Interview grilling - "Turn your adder into a subtractor without adding a second adder."
Two's complement negation is "invert every bit, then add one". The adder already has a
spare input that can supply that one: cin.
module add_sub #(parameter W = 8) (
input wire [W-1:0] a, b,
input wire sub, // 0 = add, 1 = subtract
output wire [W-1:0] result,
output wire cout
);
wire [W-1:0] b_in = b ^ {W{sub}}; // XOR with 1 inverts, with 0 passes
assign {cout, result} = a + b_in + sub;
endmodule
The cost is W XOR gates, nothing more. Note the trick on line 7:
b ^ {W{sub}} - replicating the control bit to full width turns a
conditional inversion into a single XOR, no MUX required.
Say this too: "For subtraction, cout is a
not-borrow flag - it is 1 when A ≥ B unsigned. That is exactly how the
unsigned less-than comparison gets computed for free."
2.3 Arithmetic logic units & barrel shifters
An ALU is a MUX wrapped around an adder. The interesting engineering is not the operation list - it is sharing one expensive adder across four different operations, and getting the condition flags right.
Overflow: the flag everyone gets wrong
Cout tells you about unsigned overflow. It says nothing useful about
signed arithmetic. Signed overflow has its own condition:
V = (Amsb = Bmsb) · (Smsb ≠ Amsb)
The second form is the intuitive one: adding two numbers of the same sign must produce that sign. If it does not, the result wrapped around. Adding numbers of opposite signs can never overflow, which is why the condition requires the signs to match.
| 4-bit example | Binary | Cout | V | Reading |
|---|---|---|---|---|
| 7 + 1 = 8 | 0111 + 0001 = 1000 | 0 | 1 | Signed overflow (+7+1 gave -8); unsigned fine |
| 15 + 1 = 16 | 1111 + 0001 = 0000 | 1 | 0 | Unsigned overflow; signed fine (-1+1 = 0) |
| 3 + 2 = 5 | 0011 + 0010 = 0101 | 0 | 0 | Correct in both interpretations |
Cout and
V as separate flags and lets the instruction decide which one matters."
A 32-bit RISC-V style ALU
module alu #(
parameter W = 32
) (
input wire [W-1:0] a,
input wire [W-1:0] b,
input wire [3:0] op,
output reg [W-1:0] y,
output wire zero,
output wire negative,
output wire carry,
output wire overflow
);
localparam ALU_ADD = 4'd0, ALU_SUB = 4'd1, ALU_SLL = 4'd2,
ALU_SLT = 4'd3, ALU_SLTU = 4'd4, ALU_XOR = 4'd5,
ALU_SRL = 4'd6, ALU_SRA = 4'd7, ALU_OR = 4'd8,
ALU_AND = 4'd9;
localparam SHW = $clog2(W); // 5 bits of shift amount for W = 32
// ---- ONE shared adder serves ADD, SUB, SLT and SLTU ------------------
wire is_sub = (op == ALU_SUB) | (op == ALU_SLT) | (op == ALU_SLTU);
// Conditional inversion, no MUX: replicate the control bit and XOR.
wire [W-1:0] b_in = b ^ {W{is_sub}};
wire [W:0] sum_x = {1'b0, a} + {1'b0, b_in} + is_sub;
wire [W-1:0] sum = sum_x[W-1:0];
assign carry = sum_x[W];
assign overflow = (a[W-1] == b_in[W-1]) & (sum[W-1] != a[W-1]);
// Both comparisons fall out of the subtraction for free.
wire lt_signed = sum[W-1] ^ overflow; // two's complement less-than
wire lt_unsigned = ~carry; // no carry out == borrow
wire [SHW-1:0] shamt = b[SHW-1:0];
always @(*) begin
case (op)
ALU_ADD, ALU_SUB : y = sum;
ALU_SLL : y = a << shamt;
ALU_SLT : y = {{(W-1){1'b0}}, lt_signed};
ALU_SLTU : y = {{(W-1){1'b0}}, lt_unsigned};
ALU_XOR : y = a ^ b;
ALU_SRL : y = a >> shamt; // logical: shifts in 0
ALU_SRA : y = $signed(a) >>> shamt; // arithmetic: sign extends
ALU_OR : y = a | b;
ALU_AND : y = a & b;
default : y = {W{1'b0}}; // every path assigns y -> no latch
endcase
end
assign zero = (y == {W{1'b0}});
assign negative = y[W-1];
endmodule
>>> only sign-extends on a signed operand
Writing a >>> shamt where a is a plain
wire [31:0] performs a logical shift - the operand is unsigned, so
there is no sign to extend, and the >>> silently behaves like
>>. You must write $signed(a) >>> shamt, as
above. This is one of the most common silent bugs in student ALUs.
The barrel shifter
A shift-by-k where k is a runtime value cannot be wires alone - it needs a real circuit. The naive approach shifts one position per clock, taking up to N cycles. A barrel shifter does any shift in a single cycle by decomposing the shift amount into its binary digits and building one stage per digit.
shamt. Total cost: log₂(N) stages of N MUXes, and the
delay never depends on how far you shift.
module barrel_shift_right #(
parameter W = 8,
parameter SHW = 3 // $clog2(W)
) (
input wire [W-1:0] din,
input wire [SHW-1:0] shamt,
input wire arith, // 1 = arithmetic (replicate sign), 0 = logical
output wire [W-1:0] dout
);
wire fill = arith & din[W-1]; // what gets shifted in at the top
wire [W-1:0] stage [0:SHW]; // stage[0] = input, stage[SHW] = output
assign stage[0] = din;
genvar i;
generate
for (i = 0; i < SHW; i = i + 1) begin : g_stage
// Stage i shifts by exactly 2**i, or not at all. (1<<i) is an
// elaboration-time constant, so this is a plain MUX per bit.
assign stage[i+1] = shamt[i]
? { {(1<<i){fill}}, stage[i][W-1 : (1<<i)] }
: stage[i];
end
endgenerate
assign dout = stage[SHW];
endmodule
fill bits with the bits that fall off the bottom and the barrel
shifter becomes a barrel rotator - the primitive behind
ROR/ROL instructions and every hash and crypto round function.
One structure, three instructions.
2.4 Multipliers & divider architectures
Multiplication is addition done N times in parallel. The naive array multiplier generates N partial products and sums them - correct, but both large and slow. Two independent optimisations attack it: Booth encoding reduces how many partial products exist, and Wallace trees reduce how long they take to add.
Booth's algorithm: fewer partial products
Booth's insight is that a run of consecutive 1s can be replaced by one addition and one
subtraction. 0111 (7) is 1000 - 0001 (8 - 1). Radix-2 Booth
examines each bit alongside the bit to its right:
| bi | bi-1 | Action | Meaning |
|---|---|---|---|
| 0 | 0 | no operation | Inside a run of zeros |
| 0 | 1 | A = A + M | End of a run of ones |
| 1 | 0 | A = A - M | Start of a run of ones |
| 1 | 1 | no operation | Inside a run of ones |
module booth_multiplier #(
parameter W = 8
) (
input wire clk,
input wire rst_n,
input wire start,
input wire signed [W-1:0] multiplicand,
input wire signed [W-1:0] multiplier,
output wire signed [2*W-1:0] product,
output reg busy,
output reg done
);
reg signed [W-1:0] a; // accumulator (high half of the result)
reg signed [W-1:0] q; // multiplier, becomes the low half
reg q_1; // the phantom bit to the right of q
reg signed [W-1:0] m; // multiplicand
reg [$clog2(W):0] count;
// Combinational: decide this step's operation from the Booth pair.
reg signed [W-1:0] a_next;
always @(*) begin
case ({q[0], q_1})
2'b01: a_next = a + m; // end of a run of ones -> add
2'b10: a_next = a - m; // start of a run of ones -> subtract
default: a_next = a; // 00 or 11 -> nothing
endcase
end
assign product = {a, q};
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
a <= 0; q <= 0; q_1 <= 1'b0; m <= 0;
count <= 0; busy <= 1'b0; done <= 1'b0;
end else begin
done <= 1'b0; // single-cycle pulse
if (start && !busy) begin
a <= 0;
q <= multiplier;
q_1 <= 1'b0;
m <= multiplicand;
count <= W;
busy <= 1'b1;
end else if (busy) begin
// ARITHMETIC shift right of the whole {a, q, q_1} register.
// a's MSB is replicated - that is what keeps signs correct.
a <= {a_next[W-1], a_next[W-1:1]};
q <= {a_next[0], q[W-1:1]};
q_1 <= q[0];
count <= count - 1'b1;
if (count == 1) begin
busy <= 1'b0;
done <= 1'b1;
end
end
end
end
endmodule
Trace it by hand - 3 × (-2) with W = 4
M = 0011 (+3), Q = 1110 (-2). Expected product: -6.
| Step | q[0] q_1 | Action | A after shift | Q after shift | q_1 |
|---|---|---|---|---|---|
| init | - | load | 0000 | 1110 | 0 |
| 1 | 0 0 | none | 0000 | 0111 | 0 |
| 2 | 1 0 | A - M | 1110 | 1011 | 1 |
| 3 | 1 1 | none | 1111 | 0101 | 1 |
| 4 | 1 1 | none | 1111 | 1010 | 1 |
Product = {A, Q} = 1111_1010. Interpreting as signed 8-bit:
-6. ✅ Note that only one arithmetic operation ran in four
steps - a plain array multiplier would have done four.
Radix-4 modified Booth: half the partial products
Production multipliers use radix-4, which inspects three bits at a time (overlapping by one) and processes two multiplier bits per step. That halves the partial product count from N to N/2 - the single biggest area win available.
| b2i+1 b2i b2i-1 | Partial product | How it is formed |
|---|---|---|
| 000, 111 | 0 | Zero - no hardware cost |
| 001, 010 | +M | The multiplicand as-is |
| 011 | +2M | Shift left by 1 - free, just wiring |
| 100 | -2M | Shift left, then invert and add 1 |
| 101, 110 | -M | Two's complement of the multiplicand |
Wallace trees: adding the partial products faster
Once you have the partial products, summing them with a chain of adders is O(N) again. A Wallace tree instead uses 3:2 compressors - ordinary full adders used as counters - to squash three rows into two, repeatedly, until only two remain. Those final two go into one fast carry-propagate adder.
Division: the expensive one
Division has no Booth-style shortcut. The quotient bits are genuinely sequential - you cannot know bit i without having computed bit i+1. Restoring division is the direct hardware form of long division:
module divider_restoring #(
parameter W = 8
) (
input wire clk,
input wire rst_n,
input wire start,
input wire [W-1:0] dividend,
input wire [W-1:0] divisor,
output wire [W-1:0] quotient,
output wire [W-1:0] remainder,
output reg busy,
output reg done
);
reg [W-1:0] q; // holds the dividend, becomes the quotient
reg [W-1:0] d; // divisor
reg [W:0] r; // remainder, one extra bit for the trial subtract
reg [$clog2(W):0] count;
// Shift the next dividend bit into the remainder, then TRY to subtract.
wire [W:0] shifted = {r[W-1:0], q[W-1]};
wire [W:0] diff = shifted - {1'b0, d};
assign quotient = q;
assign remainder = r[W-1:0];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
r <= 0; q <= 0; d <= 0; count <= 0; busy <= 1'b0; done <= 1'b0;
end else begin
done <= 1'b0;
if (start && !busy) begin
r <= 0; q <= dividend; d <= divisor;
count <= W; busy <= 1'b1;
end else if (busy) begin
if (diff[W] == 1'b0) begin // no borrow: the subtraction fit
r <= diff;
q <= {q[W-2:0], 1'b1}; // quotient bit is 1
end else begin // borrow: RESTORE the old remainder
r <= shifted;
q <= {q[W-2:0], 1'b0}; // quotient bit is 0
end
count <= count - 1'b1;
if (count == 1) begin busy <= 1'b0; done <= 1'b1; end
end
end
end
endmodule
a / b in RTL
Synthesis will accept it and build a full combinational divider - an enormous, painfully
slow block that will single-handedly destroy your timing. Division by a
constant power of two is fine (it is a shift). Everything else needs an
explicit multi-cycle divider like the one above, or a reciprocal multiply. This is one of
the fastest ways to blow a design review.
2.5 Comparators & parity generators
Two small structures that show up constantly, and one interview question that is asked almost every single time.
Magnitude comparators
module comparator #(
parameter W = 8,
parameter SIGNED = 0
) (
input wire [W-1:0] a, b,
output wire gt, eq, lt
);
assign eq = (a == b);
generate
if (SIGNED) begin : g_signed
assign lt = ($signed(a) < $signed(b));
assign gt = ($signed(a) > $signed(b));
end else begin : g_unsigned
assign lt = (a < b);
assign gt = (a > b);
end
endgenerate
endmodule
Write it this way. Synthesis implements a < b as a subtractor and looks at
the borrow - the same silicon you would have built by hand, but the tool gets to choose the
adder architecture based on your timing constraint.
a == b is an XNOR per bit feeding one AND tree - O(log N)
depth and no carry chain at all. a < b needs a full subtraction. If you
only need to know whether two values differ, never write <.
Parity trees
module parity #(
parameter W = 32
) (
input wire [W-1:0] data,
output wire even_parity_bit, // append this to make the total even
output wire odd_parity_bit // append this to make the total odd
);
// ^ is XOR-reduction: 1 when data contains an ODD number of ones.
// Synthesis builds a balanced XOR tree - log2(W) deep, not W.
assign even_parity_bit = ^data;
assign odd_parity_bit = ~^data;
endmodule
Interview grilling - "Count the 1s in a 32-bit vector with minimum logic delay."
This is the population count question, and it is asked constantly. The wrong answer is a loop that accumulates into a running total - that describes a 32-deep adder chain. The right answer is a balanced adder tree.
module popcount32 (
input wire [31:0] data,
output wire [5:0] ones
);
wire [1:0] s1 [0:15]; // level 1: 16 two-bit sums
wire [2:0] s2 [0:7]; // level 2: 8 three-bit sums
wire [3:0] s3 [0:3]; // level 3: 4 four-bit sums
wire [4:0] s4 [0:1]; // level 4: 2 five-bit sums
genvar i;
generate
for (i = 0; i < 16; i = i + 1) begin : g_l1
assign s1[i] = data[2*i] + data[2*i+1];
end
for (i = 0; i < 8; i = i + 1) begin : g_l2
assign s2[i] = s1[2*i] + s1[2*i+1];
end
for (i = 0; i < 4; i = i + 1) begin : g_l3
assign s3[i] = s2[2*i] + s2[2*i+1];
end
for (i = 0; i < 2; i = i + 1) begin : g_l4
assign s4[i] = s3[2*i] + s3[2*i+1];
end
endgenerate
assign ones = s4[0] + s4[1]; // level 5
endmodule
The sentence that closes it: "Five levels instead of thirty-two, because the tree is balanced. And notice the widths only grow as needed - 1, 2, 3, 4, 5, 6 bits - so the early levels are nearly free."
Expect the follow-up: "What if you had to do this every cycle at 2 GHz?" Then you pipeline it - insert flops between the tree levels. Five levels makes for a natural 2-or-3-stage pipeline. Say that unprompted and you have answered the question they were about to ask.
Volume 02 recap
| Structure | The one thing to remember |
|---|---|
| MUX tree vs cascade | Same gate count, log(N) depth vs N depth. Priority chains cost depth on purpose. |
casez |
Use it for priority encoders. Never casex - it wildcards X on inputs. |
| Carry lookahead | G and P depend only on inputs, so every carry resolves in parallel. |
| Subtraction | a + (b ^ {W{sub}}) + sub - one XOR row, no second adder. |
| Overflow | V = Cin ⊕ Cout at the MSB. Cout alone is unsigned-only. |
| Barrel shifter | One stage per bit of shamt. Delay is independent of shift distance. |
>>> |
Only sign-extends on a $signed() operand. Otherwise it is just >>. |
| Booth radix-4 | Halves partial products; every multiple is 0, ±1× or ±2× - all free shifts. |
| Division | Never write a / b in RTL unless the divisor is a constant power of two. |
| Population count | Balanced adder tree: log₂(N) levels, not N. |