Computer Architecture · Instruction Sets
Fixed-Width Instruction Encoding and Where the Fields Go
How a 32-bit instruction divides into opcode, register and immediate fields, why the register fields keep the same bit offsets across formats, and what that costs in reachable constants.
An instruction in a fixed-width ISA is a single word of known size, and every one of its bits is assigned to something. On the 32-bit formats used by RISC-V, AArch64, MIPS and SPARC, that word is exactly four bytes and the decoder never has to ask how long it is. The consequence is a machine that can start decoding several instructions in the same cycle, because instruction n always begins at an offset divisible by four from instruction n−1. The price is paid in the field budget: an opcode, two or three register numbers, and an immediate all have to fit inside those same thirty-two bits.
The interesting part of the design is not the split itself but the constraint it imposes on where the pieces may sit. Once an encoding fixes a field at a bit offset, every later addition to the ISA has to work around it, and the workarounds accumulate into the shape of the instruction set.
Six fields in thirty-two bits
The R-type format of the RISC-V base integer ISA is the cleanest example because it has no immediate at all. Bits 6:0 hold a seven-bit opcode, bits 11:7 hold the destination register, bits 14:12 hold a three-bit function selector, bits 19:15 hold the first source register, bits 24:20 hold the second source register, and bits 31:25 hold a seven-bit function selector. Every field is contiguous, no field straddles a byte boundary in a way that complicates extraction, and the whole word can be sliced with six shifts and six masks.
static inline uint32_t field(uint32_t insn, unsigned lo, unsigned width)
{
return (insn >> lo) & ((1u << width) - 1);
}
/* R-type: funct7 | rs2 | rs1 | funct3 | rd | opcode */
static inline int is_add(uint32_t insn)
{
return field(insn, 0, 7) == 0x33 /* OP */
&& field(insn, 12, 3) == 0x0 /* funct3 = ADD */
&& field(insn, 25, 7) == 0x00; /* funct7 = base */
}
Two numbers in that layout determine most of the ISA’s character. Seven opcode bits give 128 major opcodes, and five register bits give 32 names. The register count is not a coincidence: a five-bit field is the smallest power of two that comfortably covers the 16 general-purpose registers of the 32-bit designs this family grew out of, with room to double. AArch64 uses the same five-bit width for the same reason and spends one of the 32 encodings on a special meaning, so that register number 31 denotes the stack pointer in some instruction classes and the zero register in others.
The three-bit funct3 field exists because 128 opcodes are not enough for every operation once loads, stores, branches and arithmetic are counted. It selects among variants that share an opcode: the width of a load, the condition of a branch, the operation of an ALU instruction. funct7 then splits one funct3 value further, which is why integer addition and subtraction differ only in that field.
Why the register fields never move
In a fixed-width ISA, the decoder can begin reading the register file before it knows what the instruction does, provided the register numbers are always in the same place. That is the reason the five-bit rs1 field occupies bits 19:15 in the R, I, S and B formats alike, and rs2 occupies bits 24:20 in R, S and B. The opcode is at the bottom of the word, so the first thing the fetch unit can classify is the operation; the register numbers are at offsets the register file port logic can wire directly.
This is a hardware simplification with a visible software cost. Because the field positions are frozen, an instruction format that wants a larger immediate cannot simply take bits from wherever is convenient. It must take them from positions that are already the immediate in some other format, or from the opcode. The I-type format takes the top twelve bits, 31:20, which are funct7 and rs2 in the R-type. That single decision is why an immediate in a register-immediate instruction is twelve bits wide and no wider.
ARM’s 32-bit encodings make the same trade with a different field map, and the differences are instructive. AArch64 places most of its immediate in a contiguous block but varies the block position by format, so its decoders need per-format extraction logic rather than one shared slice. The register fields remain five bits at positions that vary less than the immediates do. Neither design is more correct; they are two answers to the question of how much decoder regularity to buy with how much field rigidity.
The immediate pays for the layout
Twelve bits, sign-extended, gives a range of −2048 to 2047. Every load and store offset, every addi, and every branch displacement in the base ISA lives inside that window. For a load from a structure whose field sits more than two kilobytes from the base pointer, the compiler must materialise a new base address first. This is a real constraint on code shape, not a rounding error: large stack frames and arrays indexed with runtime offsets routinely exceed it, and the fix costs an extra instruction in the address computation, as described in Addressing Modes and Effective Address Computation.
The U-type format buys width by giving up the source registers entirely. It places a twenty-bit value in bits 31:12, leaves rd where it always is, and keeps the seven-bit opcode. The value is interpreted as the upper twenty bits of a 32-bit constant with the low twelve bits zero, which pairs with a twelve-bit immediate instruction to build an arbitrary constant. Two instructions produce any 32-bit value; the pairing rules and the sign-extension trap that comes with them are the subject of Immediate Encoding and Constant Materialization.
Branch and jump formats show the compression more aggressively. A branch needs a displacement, and the low bit of a displacement is always zero because instructions are at least two-byte aligned. The B-type format therefore encodes thirteen significant bits in twelve field bits, and the J-type format encodes twenty-one significant bits in twenty. Both scramble the immediate across the word rather than keeping it contiguous, which looks arbitrary until the datapath is considered: the scrambled layout lets the branch immediate share wires with the S-type store immediate, so the sign-extension and adder logic is instantiated once instead of twice.
What the scrambled immediate costs the decoder
Scrambled fields mean the immediate cannot be extracted with one shift and one mask. Reassembling a B-type displacement requires collecting bit 31 as the sign, bit 7 as the lowest significant bit, bits 30:25 as the next six, and bits 11:8 as the remaining four, then shifting each into place. The operation is a handful of shifts and ORs, and compilers and disassemblers implement it as such.
static inline int32_t b_imm(uint32_t insn)
{
uint32_t v = ((insn >> 31) & 0x1) << 12
| ((insn >> 7) & 0x1) << 11
| ((insn >> 25) & 0x3f) << 5
| ((insn >> 8) & 0xf) << 1;
/* sign-extend from bit 12 */
return (int32_t)(v << 19) >> 19;
}
The hardware does not pay this cost per instruction in the same way, because the wiring is fixed: the scramble is a permutation of wires, and a permutation costs nothing in a datapath, only in the design’s readability. Software that decodes instructions — a disassembler, a simulator, a binary translator — does pay it, and every such tool contains a table of these extraction expressions. The same asymmetry explains why fixed-width ISAs are pleasant to implement in software and why their encodings look strange in a hex dump.
Where the format budget runs out
Seven opcode bits do not stay free for long. The base integer ISA consumes a few dozen of the 128 major opcodes, the floating-point and atomic extensions take more, and four are reserved for non-standard extensions so that a vendor can add instructions without conflicting with the standard. AArch64 has a comparable problem and solves it with a hierarchy of opcode groups: a top-level field selects a class of instruction, and a second field within that class selects the operation, so the effective opcode space is the product of the two rather than a flat 128.
The other escape hatch is to stop being fixed-width. RISC-V’s compressed extension adds sixteen-bit encodings, distinguished by the low two bits of the first halfword being anything other than binary 11. A 32-bit instruction is then any word whose low two bits are 11, and it may begin at a two-byte boundary rather than a four-byte one. That single change reintroduces the problem fixed-width encoding was designed to avoid: the fetch unit can no longer assume every instruction starts at a multiple of four, and a 32-bit instruction can straddle a four-byte boundary or a cache line. Decode logic must inspect the low bits of each halfword before it knows how far to advance.
The trade is measurable in code size. A 32-bit-only encoding spends four bytes on addi x1, x1, 1, while a sixteen-bit form spends two. For code that is instruction-fetch bound, the density matters more than the decode regularity; for code that is not, the extra decoder complexity is wasted silicon. The same tension reappears at the level of the whole front end in Decode Bandwidth and the Cost of Instruction Length.
Failure modes in the encoding itself
Three classes of bug live in the encoding rather than in the program.
Reserved and illegal encodings are not uniformly trapped. A field combination that no instruction defines may raise an illegal-instruction exception on one implementation and execute as something else on another, because the specification only guarantees behaviour for defined encodings. Compilers that emit a funct7 value from a later extension will run on a machine that implements that extension and fault on one that does not, which is why runtime feature detection exists at all.
Sign extension is the second. Every immediate in the base formats is sign-extended from its top bit, including the ones that look unsigned. A twelve-bit immediate of 0x800 is −2048, not 2048, and code that assumes otherwise produces an address two kilobytes below the intended one. The assembler will accept the literal and encode it, so the error appears at run time as a wrong address rather than a diagnostic.
The third is alignment. Fixed-width encoding makes every instruction address a multiple of four, and a jump to a misaligned address is either trapped or silently masked depending on the ISA. When compressed instructions are present the requirement relaxes to two bytes: a target at a two-byte rather than a four-byte boundary becomes legal, so only the lowest bit of a function pointer is still guaranteed zero, and code that used the higher low bits as a flag changes behaviour. AArch64 sidesteps the issue by requiring four-byte alignment unconditionally, at the cost of density.
None of these are exotic. They are the direct consequence of assigning every bit of a word to a purpose and then promising, in the specification, exactly which purposes are legal.
References
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA — RISC-V International
- Arm Architecture Reference Manual for A-profile architecture — Arm Limited
- Computer Organization and Design: The Hardware/Software Interface — Patterson & Hennessy, Morgan Kaufmann