Out-of-Ordersuperscalarissue widthexecution portscontentionthroughput

Superscalar Execution

A pipelined core finishes one instruction per cycle at best. A superscalar core has several execution units and finishes several — provided your instructions need different units and do not depend on each other. Port contention is why the theoretical peak is theoretical.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
What actually determines how many instructions my core can complete in one cycle?
What you wrote
The processor works through instructions at some rate measured in gigahertz. More gigahertz, more instructions per second.
What the hardware does
Each cycle the scheduler may issue several operations, each to a specific execution port. Ports are specialised — some do integer arithmetic, some do floating point, some do loads, some do stores — so the mix of instruction *types* determines how many can go at once, independently of how many are ready.
It explains a common plateau: a loop that stops improving no matter how much you unroll it. If every operation in the loop body needs the same port, the core issues one per cycle regardless of how much independent work you supply.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Width is a mix, not a number

It is tempting to describe a core as "four-wide" and treat that as four of anything per cycle. Real cores distribute their execution units across ports with different capabilities. There may be several ports that can do simple integer arithmetic but only one that can do integer division, two that can issue loads but only one that can issue a store address, and a separate set for vector operations.

So the achievable rate for a specific loop depends on the *shape* of its instruction mix relative to the port layout. A loop of independent integer adds may saturate the ALU ports and reach a high rate. A loop of independent divisions may reach one per several cycles, because there is one divider and it is not fully pipelined. Both loops contain nothing but independent work; only the port they need differs.

This is the third distinct limit on throughput, after dependencies (Dependency Graphs: The Real Shape of Your Code) and the front end. It is also the one that unrolling cannot fix — unrolling supplies more independent work, and port contention is precisely the situation where more independent work has nowhere to go.

Two independent adds and one load issue together; the second load waits for the single load port. SIMPLIFIED two-stage model.
issueexecSIMPLIFIED
1234
add r1, r2, r3 [ALU port 0]ie
add r4, r5, r6 [ALU port 1]ie
load r7, [r8] [load port]ie
load r9, [r10] [load port]ie
div r11, r12 [divider]iee
add r1, r2, r3 [ALU port 0]Issues cycle 0.
add r4, r5, r6 [ALU port 1]Different port, same cycle — this is superscalar issue.
load r7, [r8] [load port]A third port, still cycle 0.
load r9, [r10] [load port]Independent of everything, but the load port is taken. Waits one cycle.
div r11, r12 [divider]Independent, but the divider is not fully pipelined and occupies its port for several cycles.

Front end, back end, and which one is starving

A superscalar back end can only issue what the front end delivers. Fetching, decoding and — on some designs — translating instructions into internal operations all have their own width limits, and a loop whose hot code is large or badly laid out can starve a perfectly capable back end.

That gives two failure modes that look similar from a distance and require opposite fixes. Backend-bound means operations are waiting on execution resources or data: the answer is fewer dependencies, better locality or a different instruction mix. Frontend-bound means the scheduler is idle because instructions are not arriving: the answer is smaller hot code, better branch density or fewer instruction-cache misses (Your Code Is Data Too).

Modern counters expose this split directly, which is why the top-down methodology starts by asking which side is limiting before asking anything else. Guessing at this level is unusually expensive because the two fixes actively work against each other — aggressive unrolling helps the back end and hurts the front end.

Same symptom, opposite fixes
Backend-boundFrontend-bound
What is idleThe window is full; ports or data are the constraintThe window is empty; instructions are not arriving
Typical causeDependency chains, cache misses, port contentionInstruction-cache misses, large hot loop, decode limits
Effect of unrollingOften helps — more independent workOften hurts — more code to fetch
Effect of inliningCan help by removing call overheadCan hurt by growing the hot footprint
What to readDependency Graphs: The Real Shape of Your Code, Hits, Misses and What a Miss Actually CostsYour Code Is Data Too, Branch Prediction: Guessing Well Enough to Matter

What this means when you tune

MICROARCH-SPECIFICThe number of ports, which operations each supports, and which units are fully pipelined are properties of a specific core design. Vendors publish these per microarchitecture and they change between generations; nothing in this section should be assumed of a core you have not checked.

The practical consequence is that "add more independent work" is a fix with a ceiling, and the ceiling is set by the port layout. Once a loop saturates the port its critical operation needs, the only remaining moves are to use a different port — a cheaper operation, a different formulation — or to do less work per element.

The vector ports are the interesting case here. A scalar loop saturating an ALU port has one obvious escape: issue the same arithmetic through the vector units instead, processing several elements per instruction. That converts a port-contention problem into a throughput win without needing any more independent work, which is one reason SIMD: One Instruction, Many Elements is such a large lever on numeric loops.

This is also the point at which per-machine tuning stops generalising. Port counts and capabilities differ across vendors and generations; a mix tuned to saturate one layout can be unbalanced on another. Measure on the target, and treat any specific port assignment you read about as MICROARCH-SPECIFIC by default.

  • Dependency-limited — supply independent work: unroll, use several accumulators.
  • Port-limited — change the instruction mix: cheaper operations, or move the work to vector units.
  • Front-end-limited — shrink the hot code: less unrolling, less inlining, better layout.
  • Memory-limited — none of the above helps; fix locality first (Spatial Locality).

Key points

  • Superscalar means several instructions issued per cycle, but only to ports that can execute them.
  • Port contention is a distinct limit from dependency chains, and unrolling does not fix it.
  • A core is not "N-wide" for all instruction types; the mix relative to the port layout decides the rate.
  • Backend-bound and frontend-bound look similar and need opposite fixes — measure which one binds.
  • Vector units are a separate set of ports, which is why vectorising can break through a scalar port limit.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Front end → window: instructions are decoded and supplied at the front end's own width limit.
  2. 2
    Window → readiness: operations whose operands are available become candidates for issue.
  3. 3
    Candidates → port arbitration: each operation needs a specific port; the scheduler picks a compatible subset for this cycle.
  4. 4
    Port → execution unit: chosen operations execute concurrently on separate units.
  5. 5
    Non-pipelined unit → occupancy: some units accept a new operation only every several cycles, serialising that instruction type regardless of independence.
What people conclude from this — wrongly
  • "The core is four-wide, so I should get four instructions per cycle." Only if the mix matches the ports.
  • "Unrolling more will keep helping." It helps dependency limits, not port limits, and eventually hurts the front end.
  • "Independent work always issues in parallel." Independent work competing for one port issues serially.
  • "Low IPC means memory." It can equally mean port contention or a starved front end; the counters distinguish them.
  • "This port assignment I read about applies to my CPU." It applies to the microarchitecture it was documented for.

Consequences, controls and cost

What it causes
  • • A loop can plateau at a rate well below the core's headline width purely from instruction mix.
  • • Unrolling improves dependency-limited loops and can degrade front-end-limited ones.
  • • Mixing operation types in a loop body sometimes raises throughput by spreading work across more ports.
  • • Divisions and some transcendental operations serialise far more than their instruction count suggests.
  • • Tuning that balances a specific port layout does not transfer to a different core.
What you can do
  • • Determine whether the loop is front-end or back-end limited before changing anything.
  • • If port-limited on scalar arithmetic, consider vectorising to move work to different units ([[vectorization]]).
  • • Replace expensive single-port operations — division by multiplication by a reciprocal where precision allows, for example.
  • • Balance the instruction mix so that not every operation competes for the same unit.
  • • Stop unrolling once it stops helping; past that point it is only costing instruction-cache footprint.
How to see it
  • • Split stalls into frontend-bound and backend-bound categories using top-down counters before tuning.
  • • Compare achieved IPC against the core's documented issue width for the specific instruction mix in the loop.
  • • Vary the instruction mix experimentally — swap an operation for a cheaper one on a different port — and watch whether throughput moves.
  • • Unroll progressively and record where improvement stops; the plateau identifies a non-dependency limit.
  • • Re-run on each target microarchitecture, since the binding port differs between designs.
What it costs
  • • Balancing an instruction mix for a specific port layout is machine-specific tuning with a short shelf life.
  • • Replacing division with reciprocal multiplication changes floating-point results.
  • • Unrolling to expose parallelism costs code size and register pressure.
  • • Vectorising to escape a scalar port limit adds complexity and portability concerns ([[auto-vectorization]]).

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • MICROARCH-SPECIFICIssue width, port count, which operations each port supports and which units are pipelined are all specific to a core design and change between generations. Even within one vendor, performance and efficiency cores in the same package differ.
  • SIMPLIFIEDThe pipeline trace models issue and execute only, and shows instructions rather than the internal micro-operations a real decoder produces. Register renaming, dispatch queues and retirement are omitted.

Misconceptions

Claim
“A superscalar CPU executes any N instructions per cycle.”
Reality
It executes up to N *compatible* instructions per cycle. A stream of operations that all need the same port issues one per cycle no matter how wide the core is.
Claim
“If unrolling stopped helping, the loop must be memory-bound.”
Reality
It may be port-bound or front-end-bound. All three produce a plateau; only counters tell them apart.
Claim
“Instruction counts from a disassembly predict cycles on a superscalar core.”
Reality
They predict almost nothing on their own. The same count can take wildly different cycle counts depending on dependencies and which ports the instructions need.