Computer Architecture · Instruction Sets

Condition Codes, Flag Registers and the Dependency They Create

How comparison results become flags, why an implicit flag register serialises code, and how architectures that compare directly into registers avoid the hazard.

A conditional branch needs a condition, and the condition needs somewhere to live. The dominant answer in the architectures that grew up in the 1970s and 1980s is a small set of flag bits, updated as a side effect of arithmetic, read implicitly by a conditional branch instruction whose encoding carries only a selector. The answer that replaced it, in most newer ISAs, is to compare two registers into a third and branch on that register’s contents.

The two designs look like a minor difference in instruction syntax. They differ in how many values an instruction produces, whether a compare can be folded into an existing operation, and whether the scheduler can reorder a comparison past the instructions that consume its result.

The flag register as a hidden destination

An arithmetic instruction on x86 updates six flags: carry, parity, auxiliary carry, zero, sign and overflow. The instruction’s encoding does not name them, and the programmer cannot address them individually; they are a single register whose bits are read by conditional instructions and by explicit flag-manipulation instructions. A conditional move or branch carries a four-bit condition selector and reads the flags implicitly.

The encoding saving is real. A branch in the x86 one-byte map is two bytes: an opcode that selects the condition family and a displacement. A RISC-V branch spends three register bits on a function selector and two five-bit fields on the operands, and the comparison happens in the branch itself. The flag scheme moves the operand specification to a separate compare instruction and lets the branch name only the condition.

The cost is that the flag register is a destination that no register allocator can manage. Every instruction that writes flags creates a write to a shared resource, and every conditional instruction creates a read of it. On an out-of-order core this becomes a dependency that is invisible at the architectural level but must be tracked in hardware: the rename logic has to give the flags their own physical register, and instructions that write flags must allocate a new physical flag register the way they allocate a destination register. This is why some implementations rename flags rather than keeping a single architectural copy, and why the partial-flag problem exists at all: an instruction that writes only some flags must merge with the previous value, which is a read-modify-write on the flag state and therefore an extra dependency.

Partial writes are the sharp edge. x86’s inc and dec update all flags except carry, so a sequence that computes a carry with add and then increments a pointer destroys the carry that a subsequent adc needs. The compiler must either reorder the operations or use a form that does not touch flags, and the architecture provides lea for exactly this reason: it computes an address without setting any flags, so it can be used for pointer arithmetic between a flag-producing operation and a flag-consuming one.

Compare-into-register and the extra instruction

RISC-V has no flag register. A comparison is an instruction that writes a general register with 0 or 1, and a branch compares two registers directly. AArch64 has a middle design: most instructions can optionally set flags, and conditional branches read the flags, but the flags are set by the instruction that produces the value rather than by a separate compare whenever the compiler arranges it.

The compare-into-register design removes the hidden dependency and pays in instruction count. if (a < b) x = 1; becomes a compare and a conditional move on a flag ISA, and a compare, a branch and a move on a compare-into-register ISA unless the architecture has a conditional select. RISC-V’s base ISA has no conditional move, so the sequence is a branch. This is why conditional select was added as an extension and why AArch64 provides it in the base: without it, the absence of flags turns every small conditional into a branch.

The trade is legible in the encoding. A conditional select needs three register fields: two sources and a condition source. A flag-based conditional move needs two register fields and a condition selector, because the condition is implicit. The compare-into-register architecture spends encoding space on a third source register that the flag architecture spends on a four-bit selector, which is the same budget question raised in Two-Operand Encodings and the Destructive Write Problem.

Flags and the branch that reads them

The distance between the instruction that sets a flag and the branch that reads it is a scheduling problem, not an encoding one, but the encoding determines whether the problem exists. A flag ISA’s branch depends on the immediately preceding arithmetic even when the branch’s operands are unrelated, because the branch’s only input is the flag register. The scheduler cannot begin resolving the branch until the flag write completes, which puts the flag-producing instruction on the branch resolution critical path.

A compare-into-register ISA has the same dependency expressed as a register dependency, and the scheduler can see it. The difference is that the compiler can also break it: a compare whose result is used by a conditional move rather than a branch can be scheduled far from the branch, and the comparison’s result can be reused by several consumers. With flags, the value is only readable through the branch and is overwritten by the next arithmetic instruction.

This is the mechanism behind the general rule that flag architectures produce more branch-heavy code. Each comparison must be immediately followed by its consumer, so the compiler has less freedom to merge conditions or to compute several predicates before using them. Code that computes four predicates and then selects among eight cases is natural on a compare-into-register machine and awkward on a flag machine, where each predicate must be consumed before the next is computed unless it is saved with setcc into a byte register — which is itself an instruction that reads flags and writes a register.

Predication as a substitute for branching

An instruction that executes conditionally, without a branch, removes the branch entirely. The encoding carries a predicate register number or a condition selector, and the instruction’s write is suppressed when the predicate is false.

The fully predicated form, where every instruction takes a predicate operand, is expensive in encoding space: a four-bit predicate field on every instruction is a fixed tax. Architectures that use it either accept the tax or restrict predication to a subset of instructions. The restricted form is more common: a small set of conditional move and conditional select instructions, plus a conditional form for a few others.

The benefit is not the removal of the branch’s instruction fetch but the removal of the misprediction penalty. A branch that is hard to predict costs the pipeline refill on the wrong path, which on a deep pipeline is tens of cycles; the magnitude depends on the pipeline depth and the misprediction recovery mechanism, both of which belong to the microarchitecture rather than to the encoding. A predicated instruction costs one issue slot whether or not it executes. For a condition that is unpredictable and a body that is short, predication wins by a wide margin; for a body that is long or contains memory accesses, predication executes work that a branch would have skipped, and the branch wins.

There is a semantic trap in predication that has nothing to do with performance. A predicated instruction that would fault must not fault when its predicate is false, or the predication does not provide the safety the branch provided. Architectures that guarantee this implement the check before the memory access; architectures that do not guarantee it require the compiler to prove that the address is valid on both paths, which usually means falling back to a branch. The guarantee is a property of the architecture, not of the instruction encoding, but it determines whether the encoding is usable.

Flags across calls and across exceptions

The flag register is architectural state, which means it must be saved and restored wherever architectural state is saved.

On a call, the flags are caller-saved on x86: a function may clobber them, and a caller that needs a flag across a call must save it. This is why the calling convention lists flags among the clobbered state and why compilers rarely keep a condition live across a call. On a compare-into-register architecture the condition is an ordinary register, so it follows the ordinary caller-saved rules and can be kept live in a callee-saved register if it is valuable enough.

On an exception, the flags are part of the saved context, and the handler’s first instruction typically clobbers them. The architecture defines whether the flags at the fault point are the ones the faulting instruction would have produced or the ones from before it; the answer matters for restart, because a handler that emulates the faulting instruction must reproduce the flag result exactly.

The most awkward case is a flag-setting instruction that faults. A memory-destination read-modify-write such as add [mem], reg loads the location, computes, and stores the result; if the store faults — because the page is read-only, say — the architectural state must be as it was before the instruction, with the memory location and the flags both unchanged, so that the fault handler can re-execute it once the condition is fixed. x86 specifies the restart point per instruction, and the flags must be consistent with that point. String instructions are the documented exception: a repeated move or compare can complete part of its work before a fault, and the registers and flags then reflect how far it got. This is the kind of detail that a load-store architecture avoids by construction, as discussed in Addressing Modes and Effective Address Computation.

Which design a new ISA should choose

The choice is settled by what the encoding can afford and what the pipeline needs.

A flag register costs nothing in encoding space for the arithmetic instructions, because the flags are a side effect of an operation the instruction was already performing. It costs a hidden dependency, a partial-write hazard, and a serialisation between comparison and consumer. For a machine with a short pipeline and a simple scheduler, those costs are small, and the encoding saving is real.

A compare-into-register design costs an instruction when the comparison is not already part of an arithmetic operation, and it costs a third source register field when conditional select is provided. It buys a visible dependency that the scheduler can rename, a condition that can be computed once and used many times, and a clean exception model. For a machine with a deep pipeline and a large reorder window, those are the properties that matter.

The historical direction is clear: architectures designed after the mid-1980s mostly chose the register form, and the ones that kept flags kept them for compatibility. The reason is not that flags are wrong but that the cost they impose is paid in exactly the part of the machine that grew fastest — the scheduler’s ability to look ahead.

A conditional branch whose condition is an ordinary register can be resolved early and speculated past; a branch whose condition is a flag written by the previous instruction cannot be resolved until that instruction completes, and every instruction in between is exposed to the branch’s latency. The encoding decides whether that exposure exists, and no amount of microarchitectural effort removes a dependency that the instruction set made architectural.

References

  1. Intel 64 and IA-32 Architectures Software Developer Manuals, Volume 1 — Intel Corporation
  2. Arm Architecture Reference Manual for A-profile architecture — Arm Limited
  3. The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA — RISC-V International