PCIe · Module 6
Throughput Calculations — From Signalling Rate to Delivered Bytes
A seven-layer model taking GT/s through encoding, width, direction, protocol overhead, and utilisation to delivered throughput. Worked derivations for Gen1 through Gen6 at several widths, why the commonly quoted headline figures differ, and how to instrument RTL to explain the gap.
Module 5 derived what one lane can carry. Module 6 established how many lanes a Link has. Eight chapters have now deferred the multiplication, always to here.
The reason for the delay is that the multiplication is the easy part. Getting a trustworthy number requires knowing what basis it is on, and most quoted PCIe throughput figures do not say.
How do generation, encoding, width, direction, protocol overhead, and utilisation combine to determine theoretical and measured PCIe throughput?
1. The Seven-Layer Stack
Layers 1–5 are exact arithmetic. Layers 6 and 7 are not calculable from the specification — they depend on the traffic and the system, and they can only be measured. §7 and §9 are about measuring them.
2. The Core Formula, and Where It Stops Applying
For Gen1 through Gen5, where the model is a signalling rate and a line-code ratio:
per_lane_bytes_per_second = GT/s × (enc_num / enc_den) ÷ 8
link_bytes_per_second = per_lane_bytes_per_second × lane_count| Generation | Rate | Line code | Efficiency |
|---|---|---|---|
| Gen1 | 2.5 GT/s | 8b/10b | 80.0000% |
| Gen2 | 5 GT/s | 8b/10b | 80.0000% |
| Gen3 | 8 GT/s | 128b/130b | 98.4615% |
| Gen4 | 16 GT/s | 128b/130b | 98.4615% |
| Gen5 | 32 GT/s | 128b/130b | 98.4615% |
| Gen6 | 64 GT/s | 1b/1b — none | 100% at the line |
3. Worked Examples
Every derivation below shows each layer, its unit, and its basis. All are per direction unless stated otherwise.
Example A — Gen3 x4
L1 8 GT/s per lane, per direction.
L2 128b/130b → 128/130 = 0.984615384615… (98.4615%).
L3 8 × 128/130 = 7.876923076923… Gb/s per lane → ≈ 7.8769 Gb/s.
L4 ÷ 8 = 0.984615384615… GB/s per lane → ≈ 984.615 MB/s.
L5 × 4 lanes = 3.938461538461… GB/s → ≈ 3938.462 MB/s.
Exact rational: 8 × (128/130) ÷ 8 × 4 = 512/130 = 256/65 GB/s.
Basis: aggregate theoretical encoding-adjusted capacity, one direction, before protocol overhead and utilisation.
Example B — Gen4 x8
L1 16 GT/s · L2 128/130 · L3 15.753846153846… Gb/s per lane
L4 1.969230769231… GB/s per lane → ≈ 1969.231 MB/s
L5 × 8 = 15.753846153846… GB/s → ≈ 15753.846 MB/s
Exact: 1024/65 GB/s. (The per-lane Gb/s and the x8 GB/s figures coincide numerically here because the ÷8 and the ×8 cancel — a coincidence of this width, not a rule.)
Example C — Gen5 x16
L1 32 GT/s · L2 128/130 · L3 31.507692307692… Gb/s per lane
L4 3.938461538462… GB/s per lane → ≈ 3938.462 MB/s
L5 × 16 = 63.015384615385… GB/s → ≈ 63015.385 MB/s
Exact: 4096/65 GB/s.
Example D — Gen2 x1
L1 5 GT/s · L2 8/10 = 0.8 · L3 4.000000 Gb/s
L4 0.500000 GB/s · L5 × 1 = 0.500000 GB/s → exactly 500 MB/s
Exact and terminating, because 8/10 divides cleanly. Gen1 x1 gives exactly 250 MB/s by the same route.
Example E — Gen6 x16
L1 64 GT/s per lane, per direction.
L2 1b/1b → factor 1. No line-code expansion.
L3 64.000000 Gb/s per lane.
L4 ÷ 8 = 8.000000 GB/s per lane — exact, no repeating fraction.
L5 × 16 = 128.000000 GB/s → 128000 MB/s, one direction, at the line.
Then the FLIT step, which has no Gen1–Gen5 counterpart. Of the 256-byte FLIT, 236 bytes carry transaction-layer content: 236/256 = 0.921875 (92.1875%).
128 GB/s × 0.921875 = 118.000000 GB/s one direction — exactly 118, as it happens.
Basis warning: 118 GB/s has already paid overhead that the 63.015 GB/s Gen5 figure has not. Dividing them is not a valid ratio. The comparable pair is the line figures: 128 vs 63.015, a ratio of 2.03125 = 2 × 130/128 — slightly more than double, precisely because the line-code penalty went away.
Summary table
| Config | L4 per lane | L5 aggregate, one direction | Both directions summed | Exact |
|---|---|---|---|---|
| Gen1 x1 | 250 MB/s | 250 MB/s | 500 MB/s | 1/4 GB/s |
| Gen2 x1 | 500 MB/s | 500 MB/s | 1000 MB/s | 1/2 GB/s |
| Gen3 x4 | ≈984.615 MB/s | ≈3938.462 MB/s | ≈7876.923 MB/s | 256/65 GB/s |
| Gen4 x4 | ≈1969.231 MB/s | ≈7876.923 MB/s | ≈15753.846 MB/s | 512/65 GB/s |
| Gen4 x8 | ≈1969.231 MB/s | ≈15753.846 MB/s | ≈31507.692 MB/s | 1024/65 GB/s |
| Gen5 x4 | ≈3938.462 MB/s | ≈15753.846 MB/s | ≈31507.692 MB/s | 1024/65 GB/s |
| Gen5 x16 | ≈3938.462 MB/s | ≈63015.385 MB/s | ≈126030.769 MB/s | 4096/65 GB/s |
| Gen6 x16 | 8000 MB/s (line) | 128000 MB/s (line) | 256000 MB/s (line) | 128 GB/s |
All figures are decimal units: 1 GB = 10⁹ bytes, 1 MB = 10⁶ bytes.
4. Direction — The Basis Most Often Omitted
PCIe lanes provide transmit and receive paths that operate at the same time (Chapter 6.1). So there are two legitimate ways to quote a figure and they differ by a factor of two.
Per direction. What one direction can carry. This is what every calculation in §3 produces at layer 5, and it is what matters when a workload moves data predominantly one way — which most workloads do.
Both directions summed. The arithmetic sum of the two. Legitimate only when the workload genuinely uses both directions simultaneously at that rate.
A rule worth adopting: never write a PCIe throughput figure without stating generation, width, direction, and whether encoding has been applied. Gen4 x8, one direction, encoding-adjusted: ≈15.75 GB/s is unambiguous. Gen4 x8: 16 GB/s is not, and both readings of it are defensible.
5. Decimal and Binary Units
MB/s and MiB/s are not interchangeable and the gap is not small.
1 MB = 10⁶ bytes,1 GB = 10⁹ bytes— decimal, used throughout this curriculum and by PCI-SIG.1 MiB = 2²⁰ = 1 048 576 bytes,1 GiB = 2³⁰ bytes— binary, used by many operating-system tools.
Converting Example A: 3938.462 MB/s = 3 938 461 538 B/s ÷ 1 048 576 = 3756.010 MiB/s.
The ratio is 10⁶ / 2²⁰ = 0.953674, a 4.63% difference — larger than many of the effects engineers spend days chasing. A measured figure reported in MiB/s and compared against a theoretical figure in MB/s will look 4.63% low for no reason at all.
6. Protocol Overhead — Layer 6
Layer 5 gives what the Link can put on the wire. It is still not payload.
Categories, conceptually:
- Transaction headers. Every operation carries descriptive information alongside its data.
- Link-layer and control information. Traffic that manages the Link rather than carrying transactions.
- Framing and protection. Delimiting and protecting what is sent. At Gen6 this is the FLIT's CRC and FEC regions; at earlier generations it is framing and per-packet protection.
- Non-data time. Intervals where nothing is being transmitted.
- Acknowledgement and flow-control traffic. The mechanisms that make the Link reliable.
Exact field-level accounting is not this chapter's subject. Modules 10–13 own transaction-layer packets and Modules 14–17 own the data link layer. What matters here is the conclusion:
Encoding-adjusted capacity is not payload throughput. Layer 5 is an upper bound that no workload reaches.
Transaction size and efficiency
The most important layer-6 effect is one every engineer can reason about without any specification detail.
If every transaction carries a fixed overhead H and a payload P, the fraction of transported bytes that are payload is:
efficiency = P / (P + H)With a hypothetical H = 24 bytes:
| Payload P | P / (P + 24) | Efficiency |
|---|---|---|
| 64 B | 64/88 | 72.73% |
| 128 B | 128/152 | 84.21% |
| 256 B | 256/280 | 91.43% |
| 512 B | 512/536 | 95.52% |
| 1024 B | 1024/1048 | 97.71% |
| 4096 B | 4096/4120 | 99.42% |
Generic transaction-efficiency model — the value of H is hypothetical and is not a PCIe figure. It is chosen to make the shape legible.
The shape is what transfers, and it is general: fixed overhead hurts small transfers disproportionately, and the penalty falls off quickly as payload grows. A workload of 64-byte accesses forfeits roughly a quarter of the transport to overhead before anything else goes wrong; the same Link carrying 4 KB transfers forfeits well under one percent.
7. RTL — The Capacity Package
// COMPILE-TIME ONLY (elaboration constants and pure functions).
// Integer/rational arithmetic; no `real`. NOT PCIe controller RTL.
package pcie_capacity_pkg;
typedef enum int unsigned { GEN1, GEN2, GEN3, GEN4, GEN5, GEN6 } pcie_gen_e;
// Basis marker. A capacity figure without its basis is not comparable with
// any other figure, so the basis travels WITH the number.
typedef enum int unsigned {
BASIS_LINE_AFTER_ENCODING, // Gen1-Gen5 convention: encoding applied
BASIS_LINE_UNENCODED, // Gen6: 1b/1b, nothing to apply
BASIS_FLIT_TLP_REGION // Gen6 after the FLIT's non-TLP bytes
} capacity_basis_e;
// Signalling rate in MEGA-transfers/second. Integer at every generation,
// which is why this unit is used rather than GT/s.
function automatic int unsigned gen_mtps(pcie_gen_e g);
case (g)
GEN1: return 2_500;
GEN2: return 5_000;
GEN3: return 8_000;
GEN4: return 16_000;
GEN5: return 32_000;
GEN6: return 64_000;
default: return 0;
endcase
endfunction
// Line-code ratio as an exact rational. Gen6 is 1/1: unencoded (1b/1b).
// Written as an explicit ratio so the CONTRAST with Gen3-Gen5 stays visible
// rather than silently absent.
function automatic int unsigned enc_num(pcie_gen_e g);
case (g)
GEN1, GEN2: return 8;
GEN3, GEN4, GEN5: return 128;
GEN6: return 1;
default: return 0;
endcase
endfunction
function automatic int unsigned enc_den(pcie_gen_e g);
case (g)
GEN1, GEN2: return 10;
GEN3, GEN4, GEN5: return 130;
GEN6: return 1;
default: return 1;
endcase
endfunction
function automatic bit width_is_legal(int unsigned lanes);
// The widths Module 6 covers are powers of two. This model accepts only
// those; a design supporting others must relax the check deliberately.
return (lanes != 0) && ((lanes & (lanes - 1)) == 0);
endfunction
// Aggregate capacity in kilobytes/second, one direction.
//
// TWO ARITHMETIC DECISIONS THAT MATTER:
//
// 1. 64-BIT INTERMEDIATE. The numerator reaches 6.55e10 at Gen5 x16, well
// past a 32-bit range. Even Gen5 x1 reaches 4.096e9 — only 4.63% below
// the 32-bit unsigned maximum. A 32-bit intermediate here is not merely
// risky, it is wrong for the configurations most worth computing.
//
// 2. DIVIDE LAST. Multiplying by lanes BEFORE dividing keeps the truncation
// to one final operation. Computing per-lane first and then scaling
// multiplies the per-lane truncation by the lane count: at Gen5 x16 that
// is an 8.6 kB/s error instead of 0.6 kB/s.
function automatic longint unsigned link_kBps(pcie_gen_e g, int unsigned lanes);
longint unsigned num;
if (!width_is_legal(lanes)) return 0;
num = longint'(gen_mtps(g)) * 1000 * longint'(enc_num(g)) * longint'(lanes);
return num / (longint'(enc_den(g)) * 8);
endfunction
function automatic capacity_basis_e link_basis(pcie_gen_e g);
return (g == GEN6) ? BASIS_LINE_UNENCODED : BASIS_LINE_AFTER_ENCODING;
endfunction
// Gen6 only: the share of the transport unit carrying transaction content.
// Byte counts only — field order and bit layout are specification detail
// and are deliberately not modelled. See Chapter 5.6.
localparam int unsigned FLIT_BYTES = 256;
localparam int unsigned FLIT_TLP_BYTES = 236;
function automatic longint unsigned gen6_tlp_region_kBps(int unsigned lanes);
return (link_kBps(GEN6, lanes) * FLIT_TLP_BYTES) / FLIT_BYTES;
endfunction
// Elaboration self-checks. These fire at compile time if the table above is
// ever edited incorrectly — cheaper than discovering it in a reference model.
initial begin
if (link_kBps(GEN2, 1) != 500_000) $fatal(1, "Gen2 x1 != 500000 kB/s");
if (link_kBps(GEN3, 4) != 3_938_461) $fatal(1, "Gen3 x4 mismatch");
if (link_kBps(GEN4, 8) != 15_753_846) $fatal(1, "Gen4 x8 mismatch");
if (link_kBps(GEN5, 16) != 63_015_384) $fatal(1, "Gen5 x16 mismatch");
if (link_kBps(GEN6, 16) != 128_000_000) $fatal(1, "Gen6 x16 line mismatch");
if (gen6_tlp_region_kBps(16) != 118_000_000) $fatal(1, "Gen6 x16 FLIT region");
if (width_is_legal(3)) $fatal(1, "x3 must not be accepted");
end
endpackageClassification: compile-time only.
What it models: exact capacity arithmetic across generation and width, with the basis carried alongside the number.
What it teaches — three things beyond the formula:
- Overflow is a real hazard here, not a theoretical one. Gen5 x1 sits 4.63% under the 32-bit unsigned ceiling and Gen5 x16 is fifteen times past it. Anyone writing this in 32-bit arithmetic gets correct answers for small configurations and silently wrong ones for the interesting cases.
- Divide last. The truncation error at Gen5 x16 is 0.6 kB/s with one final division and 8.6 kB/s if you compute per-lane and scale. Both are small; the habit is what matters, and in other calculations the same mistake is not small.
- The basis belongs with the number.
capacity_basis_eexists so a comparison across incompatible bases can be refused rather than silently performed.
Deliberately simplified: power-of-two widths only; no representation of protocol overhead at Gen1–Gen5; kB/s granularity, which truncates by well under one part per million; and the exact rational values are not exposed.
Production implication: a real performance model additionally needs transaction-size effects, the actual negotiated width and generation rather than the intended ones, and measurement rather than calculation for anything at layer 6 or 7.
8. RTL — Utilisation
Layers 6 and 7 cannot be calculated. They must be measured, and measuring them well means classifying every cycle.
// SYNTHESIZABLE. Cycle classification and byte accounting over a measurement
// window. Illustrative observability — not required by PCIe.
module link_utilisation_monitor #(
parameter int CNT_W = 32,
parameter int BYTES_W = 12
) (
input logic clk,
input logic rst_n,
input logic window_start, // publish and restart
input logic running,
// The observed interface. Watched only — never driven.
input logic xfer_valid,
input logic xfer_ready,
input logic [BYTES_W-1:0] xfer_bytes, // bytes moved by this transfer
output logic snap_valid,
output logic [CNT_W-1:0] snap_bytes,
output logic [CNT_W-1:0] snap_active, // valid && ready
output logic [CNT_W-1:0] snap_stalled, // valid && !ready
output logic [CNT_W-1:0] snap_idle, // !valid
output logic [CNT_W-1:0] snap_cycles, // the DENOMINATOR
output logic snap_saturated
);
logic [CNT_W-1:0] c_bytes, c_active, c_stalled, c_idle, c_cycles;
logic sat_q;
// THE CLASSIFICATION. Exactly one of these holds in every counted cycle:
// active = data moved
// stalled = data was offered and the sink did not take it
// idle = nothing was offered
// Mutually exclusive and exhaustive by construction, which is what makes
// active + stalled + idle == cycles a checkable invariant rather than a hope.
wire cyc_active = xfer_valid && xfer_ready;
wire cyc_stalled = xfer_valid && !xfer_ready;
wire cyc_idle = !xfer_valid;
function automatic logic [CNT_W-1:0] sat_add(input logic [CNT_W-1:0] v,
input logic [CNT_W-1:0] inc);
logic [CNT_W:0] wide = {1'b0, v} + {1'b0, inc};
sat_add = wide[CNT_W] ? {CNT_W{1'b1}} : wide[CNT_W-1:0];
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_bytes <= '0; c_active <= '0; c_stalled <= '0; c_idle <= '0;
c_cycles <= '0; sat_q <= 1'b0;
snap_valid <= 1'b0; snap_bytes <= '0; snap_active <= '0;
snap_stalled <= '0; snap_idle <= '0; snap_cycles <= '0;
snap_saturated <= 1'b0;
end else if (window_start) begin
// ATOMIC SNAPSHOT: numerators and denominator captured in one cycle, so
// every ratio derived from them describes the same window.
snap_valid <= 1'b1;
snap_bytes <= c_bytes;
snap_active <= c_active;
snap_stalled <= c_stalled;
snap_idle <= c_idle;
snap_cycles <= c_cycles;
snap_saturated <= sat_q;
c_bytes <= '0; c_active <= '0; c_stalled <= '0; c_idle <= '0;
c_cycles <= '0; sat_q <= 1'b0;
end else if (running) begin
c_cycles <= sat_add(c_cycles, CNT_W'(1));
if (cyc_active) begin
c_active <= sat_add(c_active, CNT_W'(1));
c_bytes <= sat_add(c_bytes, CNT_W'(xfer_bytes));
end
if (cyc_stalled) c_stalled <= sat_add(c_stalled, CNT_W'(1));
if (cyc_idle) c_idle <= sat_add(c_idle, CNT_W'(1));
// Saturation must be visible. A saturated counter is not a measurement,
// and a ratio computed from one is meaningless in a way that looks fine.
if (c_cycles == {CNT_W{1'b1}} || c_bytes == {CNT_W{1'b1}} ||
c_active == {CNT_W{1'b1}} || c_stalled == {CNT_W{1'b1}} ||
c_idle == {CNT_W{1'b1}})
sat_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
What it models: where a Link's time actually went, and how many bytes crossed it, over a defined window.
What it teaches: that the three classifications answer three different diagnostic questions and only make sense together.
- High idle → the Link was not the constraint. Nothing was even offered. Widening it changes nothing.
- High stalled → data was offered and something downstream would not take it. The constraint is beyond this interface.
- High active with low payload → the Link is busy carrying something, and §9's efficiency counters are needed to find out what.
Deliberately simplified: one direction, one interface, no per-lane attribution, and no distinction between kinds of stall.
Production implication: a real implementation would instrument both directions separately, expose the operating width and generation alongside the counts so cross-configuration comparison stays valid, and provide enough windows to reveal burstiness rather than one aggregate.
9. RTL — Payload Versus Transport
Utilisation says the Link was busy. It does not say the traffic was useful.
// SYNTHESIZABLE. Counts the NUMERATOR and DENOMINATOR of transport
// efficiency. Deliberately computes NO ratio.
module payload_efficiency_counters #(
parameter int CNT_W = 32,
parameter int BYTES_W = 12
) (
input logic clk,
input logic rst_n,
input logic window_start,
input logic running,
input logic xfer, // a transfer occurred
input logic [BYTES_W-1:0] transport_bytes, // total bytes it moved
input logic [BYTES_W-1:0] payload_bytes, // of which, useful payload
output logic snap_valid,
output logic [CNT_W-1:0] snap_transport,
output logic [CNT_W-1:0] snap_payload,
output logic contract_violation // sticky: payload > transport
);
logic [CNT_W-1:0] c_transport, c_payload;
logic viol_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_transport <= '0; c_payload <= '0; viol_q <= 1'b0;
snap_valid <= 1'b0; snap_transport <= '0; snap_payload <= '0;
end else if (window_start) begin
snap_valid <= 1'b1;
snap_transport <= c_transport; // captured together: one basis
snap_payload <= c_payload;
c_transport <= '0; c_payload <= '0;
// viol_q is NOT cleared — a contract violation in an earlier window
// must not be erased by a clean one.
end else if (running && xfer) begin
c_transport <= c_transport + CNT_W'(transport_bytes);
c_payload <= c_payload + CNT_W'(payload_bytes);
if (payload_bytes > transport_bytes)
viol_q <= 1'b1; // payload cannot exceed what carried it
end
end
assign contract_violation = viol_q;
endmoduleClassification: synthesizable.
What it models: the two quantities from which transport efficiency is computed — and deliberately not the efficiency itself.
Deliberately simplified: one classification of "payload," no per-transaction-type breakdown, and non-saturating counters — which is why the window must be sized so CNT_W cannot overflow.
Production implication: saturating counters with a visible flag, breakdown by traffic class, and the operating configuration published alongside so comparisons across generations or widths remain valid.
10. Assertions
// SVA over the two monitors. Implementation invariants for THESE designs —
// not PCIe protocol requirements.
// CONSERVATION — P1: every counted cycle is classified exactly once. This is
// the property that makes the whole measurement trustworthy: if the three
// classes do not sum to the window, some cycles are unaccounted for and every
// derived percentage is wrong by an unknown amount.
property p_cycles_conserved;
@(posedge clk) disable iff (!rst_n)
(c_active + c_stalled + c_idle) == c_cycles;
endproperty
a_cycles_conserved : assert property (p_cycles_conserved);
// EXCLUSIVITY — P2: the three classifications are mutually exclusive. P1
// could hold with double-counting that happens to cancel; this closes that.
property p_classes_exclusive;
@(posedge clk) disable iff (!rst_n)
$onehot({cyc_active, cyc_stalled, cyc_idle});
endproperty
a_classes_exclusive : assert property (p_classes_exclusive);
// CORRECTNESS — P3: bytes accumulate only on an actual handshake. Counting
// on `valid` alone inflates throughput by every stalled cycle — a bug that
// makes a congested Link look fast.
property p_bytes_only_on_transfer;
@(posedge clk) disable iff (!rst_n)
(c_bytes != $past(c_bytes)) |-> $past(xfer_valid && xfer_ready);
endproperty
a_bytes_on_xfer : assert property (p_bytes_only_on_transfer);
// CONSERVATION — P4: counters are monotonic within a window.
property p_counters_monotonic;
@(posedge clk) disable iff (!rst_n)
(running && !window_start) |=> (c_bytes >= $past(c_bytes))
&& (c_cycles >= $past(c_cycles));
endproperty
a_counters_monotonic : assert property (p_counters_monotonic);
// SAFETY — P5: saturation is reported. A saturated counter is not a
// measurement; a ratio computed from one looks entirely plausible.
property p_saturation_visible;
@(posedge clk) disable iff (!rst_n)
(c_cycles == {CNT_W{1'b1}} && running && !window_start) |=> snap_saturated
|| sat_q;
endproperty
a_saturation_visible : assert property (p_saturation_visible);
// SAFETY — P6: payload never exceeds transport. Catches a payload count that
// includes header bytes, which produces efficiencies above 100% — obviously
// wrong at 105% and invisible at 99%.
property p_payload_bounded;
@(posedge clk) disable iff (!rst_n)
(running && xfer) |-> (payload_bytes <= transport_bytes) || contract_violation;
endproperty
a_payload_bounded : assert property (p_payload_bounded);
// STABILITY — P7: the snapshot is coherent. Numerators and denominator must
// describe one window, or every ratio derived from them is fiction.
property p_snapshot_coherent;
@(posedge clk) disable iff (!rst_n)
(snap_valid && !window_start) |=> ($stable(snap_bytes) && $stable(snap_cycles)
&& $stable(snap_active) && $stable(snap_idle));
endproperty
a_snapshot_coherent : assert property (p_snapshot_coherent);
// LEGALITY — elaboration-time checks on the capacity package's inputs.
// These belong at compile time, not runtime; listed here for completeness.
// enc_den(g) > 0, enc_num(g) <= enc_den(g), lanes > 0 and a power of two.P1 is the highest-value property in this chapter. Every percentage a performance engineer quotes — utilisation, stall fraction, idle fraction — is a count divided by the window. If the classes do not sum to the window, the percentages do not sum to 100% and the discrepancy is silent. The numbers still look reasonable, and the conclusions drawn from them are wrong.
P3 catches a bug that makes performance look better than it is. Counting bytes on valid rather than valid && ready credits every stalled cycle with a transfer that did not happen. The measured throughput exceeds what the Link delivered, and the direction of the error is the one nobody questions.
P6's failure is worse when it is small. A payload count that mistakenly includes a few header bytes produces an efficiency of 101% — obviously wrong. One that includes them in a way that keeps the result under 100% is undetectable by inspection, and it makes every efficiency figure optimistic by a fixed unknown amount.
11. Verification
Monitors observe: the watched interface's handshake, byte counts, the running and window indications, and every snapshot field.
The scoreboard independently predicts: expected byte totals from its own tally of observed handshakes; expected cycle classification from its own evaluation of valid and ready; and expected theoretical capacity from its own implementation of the layer 1–5 arithmetic.
Scenarios:
- Fully saturated.
validandreadyboth held high for the window. Expectactive == cycles,stalled == idle == 0, and byte total equal tocycles × bytes_per_transfer. This calibrates everything else. - Fully idle.
validlow throughout. Expectidle == cyclesand zero bytes. - Fully stalled.
validhigh,readylow throughout. Expectstalled == cyclesand zero bytes — the case P3 exists for. - Mixed traffic. Randomised
valid/ready. Verify P1 holds every cycle and the scoreboard's independent tally matches. - Single-cycle transfers.
readyasserted for exactly one cycle at a time. Catches classification logic that assumes bursts. - Variable transfer sizes. Sweep
xfer_bytesincluding its minimum and maximum. Verify byte accumulation is exact. - Counter saturation. Drive a counter to its maximum. Verify saturation, the flag, and no wrap.
- Payload equal to transport, and payload zero. Both boundary cases of the efficiency contract.
- Payload exceeding transport. Deliberately violate the contract. Verify
contract_violationsets and stays set — a property that has never been observed to fire has not been shown to work. - Window boundary races. Transfers in the same cycle as
window_start, and in the cycles either side. Verify each lands in exactly one window, never both and never neither.
Coverage should include: each classification as the dominant one; every xfer_bytes value at the boundaries; window lengths from one cycle to long; window_start coincident with each classification; and reset at each counter state.
12. Debugging — A Worked Case
Reported: "we expected 7 GB/s and measured 3.2 GB/s."
Neither number has a basis attached, so the first steps are not about PCIe at all.
1. Are the units the same? Decimal against binary is a 4.63% error before anything else. Bits against bytes is a factor of eight. A "7" and a "3.2" that differ by 2.19× are not obviously explained by unit confusion, but the check is free and it eliminates a whole class of non-problems.
2. Where did the 7 come from? From §3, Gen4 x4 = ≈7.877 GB/s one direction, encoding-adjusted. If the expectation is that figure, it is a layer 5 number, and no workload reaches layer 5 — protocol overhead and utilisation are still ahead of it. The expectation was never achievable.
3. Is the generation what you think? Verify rather than assume. A Link operating at Gen3 instead of Gen4 halves layer 5 immediately.
4. Is the width what you think? Same reasoning. A device in an x8 connector may be operating at x4 or narrower.
5. Is the expectation one direction or both summed? If the 7 came from a bidirectional headline and the measurement is one direction, the comparison is off by 2× before any real effect is considered.
6. Is the Link busy? Read snap_idle. High idle means the Link was never the constraint and nothing about it explains the shortfall. This single measurement resolves a large fraction of throughput investigations.
7. Is it stalling? Read snap_stalled. High stall means data was offered and something downstream refused it — the constraint is past this interface, not in it.
8. How large are the transfers? Read snap_payload / snap_transport. From §6, small transactions can forfeit a quarter of the transport before anything is wrong. An efficiency near 73% points straight at access size.
9. Is the upstream path narrower? From Chapter 4.4, any narrower or slower segment caps everything behind it.
10. Is the source generating enough demand, and can the destination absorb it? A device that cannot produce traffic at rate leaves capability unused regardless of transport.
11. Is memory or another system resource limiting? Data originates and terminates somewhere.
13. Common Misconceptions
- "GT/s equals GB/s." GT/s counts signalling transfers. Getting to bytes requires the encoding factor and a division by eight. At Gen3 the two differ by roughly a factor of eight and by the unit.
- "x8 means eight times x1 application throughput." It means eight times the lane count, which multiplies layer 5. Layers 6 and 7 are unchanged by width, so delivered throughput scales correspondingly only when the Link was the limiting resource and the rest of the path can sustain the demand.
- "Full duplex doubles the rate." It provides a second, independent direction. A workload moving data predominantly one way gets the one-direction figure; only traffic genuinely using both simultaneously realises the sum.
- "Encoding-adjusted capacity is payload throughput." Layer 4 has paid for the line code and nothing else. Headers, control traffic, framing, protection, and idle time are all still ahead of it.
- "A newer generation always improves application throughput." It raises layer 5. If the constraint is at layer 6 or 7 — small transactions, an idle source, a narrower upstream segment, memory — the improvement does not reach the application.
- "Link width matters even when the source is idle." If nothing is offered, no width helps.
snap_idleis the measurement that settles this, and it settles it immediately. - "One throughput number is meaningful on its own." Without generation, width, direction, and whether encoding has been applied, a PCIe figure has at least four defensible readings that differ by more than 2× in total.
- "MB/s and MiB/s are interchangeable." They differ by 4.63%. A measured MiB/s figure compared against a theoretical MB/s figure looks 4.63% low for no reason.
- "A short burst measures steady-state throughput." A brief window can be dominated by warm-up, by a queue that was already full, or by burst behaviour that cannot be sustained.
snap_cyclesis the denominator that says whether the sample is large enough to mean anything. - "Gen6 uses the same formula as Gen5." There is no 128b/130b at 64 GT/s. Applying it produces a number that is wrong and precise. Gen6's overhead is at layer 6, inside the FLIT, and a Gen6 post-FLIT figure is not on the same basis as a Gen5 post-encoding one.
14. Understanding Check
15. What's Next
This chapter produces numbers. It does not say whether the width that produced them was worth what it cost.
Chapter 6.8 — Real System Trade-offs closes Module 6 with that question. Lanes are finite, and every lane committed to one Link consumes PHY resources, package connectivity, board routing, power, and budget that could have connected or accelerated something else. Given a workload, a topology, and a lane budget, where is width actually worth spending?