Computer Architecture · Instruction Sets
Addressing Modes and How an Effective Address Is Computed
How base, index, scale and displacement fields combine into an address, why some ISAs fold memory operands into arithmetic, and what each mode costs in the datapath.
Every load and store has to name a memory location, and the instruction set has to say how. The simplest possible rule is that the address is a register’s contents, full stop. The most elaborate rule in common use combines two registers, shifts one of them by a small constant, adds a displacement, and applies a segment base — all before the memory access begins. The gap between those two rules is the addressing-mode design space, and it is one of the few places where the ISA directly determines the number of instructions a compiler must emit for ordinary code.
The quantity the instruction ultimately produces is called the effective address. Everything an addressing mode does is specify how to compute it, and the cost of a mode is the amount of arithmetic and wiring that computation needs.
The displacement form as the base case
A load in the RISC-V base ISA takes a base register and a twelve-bit signed displacement. The effective address is the register’s contents plus that constant, and the constant is sign-extended before the addition. AArch64 uses a scaled form: the immediate in a load is multiplied by the access size, so a sixty-four-bit load can reach 32760 bytes from the base with a twelve-bit scaled field, while a byte load reaches only 4095.
The scaled encoding is a compression trick. Because a four-byte load can only target an address divisible by four, the bottom two bits of its displacement are always zero and there is no reason to store them. Encoding the displacement in units of the access size gives fifteen significant bits in twelve field bits. The restriction that follows is that the displacement must be a multiple of the access size; a misaligned offset requires the address to be formed in a register first.
This is why structure field access is cheap and array indexing is not. A struct’s layout is fixed at compile time, so a field at offset 40 from a base pointer is a displacement the encoder can represent directly. An array index is a runtime value, and a displacement field cannot hold it. The compiler must therefore emit an add into a temporary register and then a load with zero displacement, which is one extra instruction per indexed access unless the ISA offers an indexed mode.
The width of the displacement field is a consequence of how the rest of the word is allocated, which Fixed-Width Instruction Encoding and Where the Fields Go works through. A twelve-bit field is what remains after the opcode and two register numbers have taken their share.
Index, scale and the two-register form
The x86 addressing form combines a base register, an index register, a scale of 1, 2, 4 or 8, and a displacement. The effective address is base + index × scale + displacement. That single mode covers array indexing with element sizes of one to eight bytes and structure field access in the same encoding, which is why x86 code for a[i].field is often a single instruction where a displacement-only ISA needs two.
The scale factors are not arbitrary. They are the sizes of the scalar types the architecture was designed around: byte, word, doubleword, quadword. A vectorised loop over float needs a scale of four, over double a scale of eight, and both are present. An array of a sixteen-byte struct falls outside the set, and the compiler shifts the index explicitly.
RISC-V omits the scaled-index form from its base ISA. Indexed access is expressed as a shift or an add into a register followed by a displacement load. The reasoning is a datapath argument: the address generation unit then needs one adder rather than an adder, a shifter and a second port on the register file, and the extra instruction is absorbed by a wide superscalar core that has issue slots to spare. Whether that reasoning holds depends on the workload. Pointer-chasing code with few arithmetic operations per load feels the extra instruction; code with a high arithmetic-to-memory ratio does not.
The trade appears clearly in the encoding budget. An x86 instruction with base, index, scale and displacement needs a ModR/M byte, a SIB byte and up to four displacement bytes on top of the opcode, so the same operation occupies up to eight bytes. A RISC-V load is four bytes, and the second instruction that computes the index is another four. The two sequences are comparable in size, but only one of them has a fixed length.
Post-increment, pre-decrement and the pointer walk
A family of modes modifies the base register as part of the access. Post-increment loads the addressed value and then adds the access size to the base. Pre-decrement subtracts first and then accesses. Both exist to make pointer walks one instruction instead of two, and both create a write to a register that the instruction also reads.
The cost is a second write port on the register file, or a serialisation of the read and the write within the same instruction. On a simple pipeline the base register update happens in the same stage as the address computation, so the mode is nearly free. On an out-of-order core the update is a register write like any other, subject to the same dependency tracking, and the instruction is simply an instruction with two destinations. The savings are real for tight copy loops, where the alternative is an add between every load and store.
The mode’s hazard is subtler than the port count. If the base register is also the destination of the loaded value, the result is implementation-defined on some architectures and an illegal encoding on others. The restriction exists because the two writes would target the same architectural register, and the specification has to pick an order. ARM’s load-with-writeback encodings forbid the base register from appearing as the destination for exactly this reason.
Folded memory operands and the RISC distinction
An x86 add can take a memory operand: the source may be a memory location, and the effective address computation happens as part of the add. The instruction reads memory, reads a register, adds, and writes the register. A load-store ISA splits that into a load and an add.
The trade is instruction count against regularity. The folded form reduces the number of instructions the front end must fetch and decode, which matters when decode bandwidth is the limit. It also lengthens the pipeline’s dependency chain in a way that is easy to underestimate: the memory read is on the critical path of the arithmetic, so a dependent operation cannot begin until the load completes, and the scheduler has less freedom to move work around it. The split form exposes the load and the add as separate operations, which an out-of-order scheduler can overlap with other work.
There is a third consideration that decided the question for most RISC designs: exceptions. A folded memory operand can fault during an instruction that also modifies a register, and the architectural state at the fault point must be defined precisely. Some x86 instructions with memory operands have writeback behaviour that is specified per instruction rather than by a general rule, which is a documentation burden that a load-store split avoids entirely.
Modes that cost more than they save
Several addressing modes exist in older architectures and have been dropped from modern ones for reasons worth understanding.
The absolute mode, where the instruction contains a full address, is convenient for accessing globals and requires no register. On a 32-bit ISA it fits in the instruction; on a 64-bit ISA it does not, and what remains is a displacement from a fixed base that the instruction still has to encode. Position-independent code makes absolute addressing unattractive anyway, because a load from a fixed address requires a relocation at load time.
The register-indirect-with-displacement form is universal, but the indirect form with no displacement is worth separating. When the displacement field is zero, the mode costs an adder that adds zero. Some ISAs provide a distinct encoding that skips the adder, and the benefit is a shorter dependency path in the address generation unit rather than a shorter instruction.
Double indirection — a memory operand whose address comes from memory — exists on a few architectures. It requires two dependent memory accesses for one instruction, which makes the instruction’s latency variable and unpredictable and complicates restart after a page fault in the middle. The mode is rare in compiled code because the compiler can always split it into two loads.
Where the address computation breaks
The effective address is computed in the address size of the mode, not the operand size. A 32-bit mode computes a 32-bit address and zero-extends it, which is why 32-bit code on a 64-bit machine cannot reach above 4 GiB and why the address-size prefix matters. Confusing operand size with address size produces truncation that is silent on values below 4 GiB and catastrophic above it.
Sign extension of the displacement is the second trap, and it is the same rule that governs arithmetic constants in Immediate Encoding and How Arbitrary Constants Get Materialized. A twelve-bit field holding 0x800 is −2048. When a compiler emits a displacement that it computed as an unsigned offset, the value must fit in eleven bits plus sign, not twelve. Assemblers accept the out-of-range value and encode the low bits, so a structure larger than two kilobytes with a field near the end produces an access to a completely different address rather than a diagnostic.
The third is the interaction with the memory model. An effective address computation does not access memory, so a fault in the address arithmetic itself — an overflow in a mode that checks — is reported differently from a fault on the access. Architectures that trap on address overflow must define whether the trap is taken before or after the register update in a writeback mode, and the answer differs between architectures.
Alignment is the fourth. Many modes permit any effective address, and the alignment requirement is attached to the access size rather than the mode. A four-byte load from an address that is not a multiple of four is legal on x86 and traps or is emulated on some RISC designs. The reason is that the hardware’s load path is built for aligned access, and supporting misalignment means either a slower general path or a trap-and-fixup handler. The addressing mode does not determine this, but it determines how often the situation arises: a scaled index mode that multiplies by four makes misalignment impossible for four-byte accesses, while an unscaled index can produce it freely.
The choice among these forms is settled early in an ISA’s life and is very hard to change later, because every compiler, assembler and binary in existence encodes the current set. What survives in the architecture is not the mode that is most expressive but the mode that the datapath can implement without lengthening the critical path of the load.
References
- Intel 64 and IA-32 Architectures Software Developer Manuals, Volume 1 — Intel Corporation
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA — RISC-V International
- Computer Architecture: A Quantitative Approach — Hennessy & Patterson, Morgan Kaufmann