Part IIInstruction Set Architectures

Addressing Modes Side-by-Side

September 8, 202633 min readintermediate

Every instruction that touches memory must answer a fundamental question: where is the data? The answer is an addressing mode, and the set of addressing modes an ISA offers shapes everything from compiler code…

Every instruction that touches memory must answer a fundamental question: where is the data? The answer is an addressing mode, and the set of addressing modes an ISA offers shapes everything from compiler code generation to hardware decoder complexity. Consider loading a 32-bit integer from an array. On RISC-V, the compiler emits lw a0, 0(a1), specifying the address as a base register (a1) plus a 12-bit signed offset (0). On AArch64, the same load might use LDR W0, [X1, X2, LSL #2], which adds X1 to X2 shifted left by two, computing the array address in a single instruction. On x86-64, the equivalent is mov eax, [rbx + rcx*4], using a base register, an index register, and a scale factor all encoded in the SIB byte described in Chapter 17.

Each of these three ISAs provides a different menu of addressing modes, and the differences reflect deep design choices about encoding simplicity, hardware cost, and compiler convenience. This chapter lays the modes side by side, explains how each ISA encodes them, and examines the tradeoffs that each choice creates.

01.A Taxonomy of Addressing Modes

Before comparing ISAs, it helps to have a uniform vocabulary. The following classification covers the modes that appear across RISC-V, AArch64, and x86-64. Not every ISA supports every mode.

The simplest ISAs support only modes 1 through 3 and mode 7. The most complex support all nine. The number of modes an ISA offers is one of the clearest markers on the RISC-versus-CISC spectrum described in Chapter 14.

02.Immediate Operands

An immediate is a constant encoded in the instruction itself. Every ISA provides some form of immediate operand for arithmetic, logical, and comparison instructions. The differences lie in the width of the immediate field and the mechanisms available when the desired constant does not fit.

RISC-V immediates

RISC-V encodes immediates in several fixed formats. I-type instructions carry a 12-bit sign-extended immediate, giving a range of −2048-2048 to +2047+2047. S-type (store) instructions split the same 12-bit immediate across two non-contiguous fields. U-type instructions (LUI and AUIPC) carry a 20-bit immediate that fills bits 31 down to 12 of a 32-bit value, with the lower 12 bits zeroed.

To load an arbitrary 32-bit constant, the compiler pairs LUI (which sets the upper 20 bits) with an ADDI (which fills the lower 12 bits). The two-instruction sequence is a direct consequence of the fixed 32-bit instruction length: there is no room for a 32-bit immediate inside a single 32-bit instruction. Chapter 15 discussed this tradeoff in detail.

AArch64 immediates

AArch64 provides a 12-bit unsigned immediate for arithmetic instructions, optionally shifted left by 12 bits. The shifted form allows common page-aligned constants to be expressed in one instruction. Logical instructions use a separate “bitmask immediate” encoding that can represent any repeating bit pattern of length 2, 4, 8, 16, 32, or 64, covering a wide range of masks and bit-field constants that would otherwise require two instructions.

The MOVZ, MOVK, and MOVN instructions build arbitrary 64-bit constants by moving, inserting, or negating 16-bit halfword chunks. A 64-bit constant with only one non-zero halfword takes one instruction. A fully arbitrary 64-bit constant takes up to four instructions.

x86-64 immediates

x86-64 allows 8-bit, 16-bit, and 32-bit immediates encoded directly after the opcode and ModR/M bytes. In 64-bit mode, the MOV instruction has a special encoding that accepts a full 64-bit immediate (opcode B8+rd, 10 bytes total with REX.W). No other x86-64 instruction takes a 64-bit immediate. All other ALU instructions sign-extend a 32-bit immediate to 64 bits, covering the range −231-2^{31} to +231−1+2^{31} - 1.

The variable-length encoding gives x86 more flexibility in immediate width than the fixed-length RISC ISAs, but at the cost of decoder complexity: the decoder must determine the immediate width from the opcode and prefix context, not from a fixed bit position.

03.Register Addressing

Register addressing is the simplest mode: the operand is the contents of a register named in the instruction. Every ISA supports register addressing, and it is the default mode for ALU operations on all three ISAs.

On RISC-V, the R-type format dedicates three 5-bit fields (rd, rs1, rs2) to name any of the 32 GPRs. On AArch64, three 5-bit fields address any of the 31 GPRs plus the zero register or stack pointer. On x86-64, the 3-bit reg and r/m fields in the ModR/M byte (extended to 4 bits by REX) address the 16 GPRs.

The practical consequence is that RISC-V and AArch64 can name three registers per instruction using 15 bits, while x86-64 needs the REX prefix to address more than eight registers and is limited to two explicitly named operands per instruction (the VEX and EVEX prefixes extend this to three or four for SIMD instructions, as discussed in Chapter 17).

04.Base-Plus-Offset Addressing

Base-plus-offset (also called base-plus-displacement) is the workhorse addressing mode for memory access on all three ISAs. The effective address is the sum of a base register and a constant offset encoded in the instruction:

Figure 1
Figure 1. Base-plus-offset address generation, which is the equation above drawn as hardware. One register read, one sign-extension of the immediate, and one addition produce the effective address before the memory access begins. The width of the immediate field is the whole story: twelve signed bits reach 2 KB around the base register, which covers ordinary stack frames and little else.

Figure 1 shows that computation as the small piece of hardware it is.

RISC-V base + offset

RISC-V load instructions use the I-type format: lw rd, imm(rs1). The 12-bit signed offset gives a range of ±\pm2 KB around the base register. Store instructions use the S-type format with the same effective-address computation but the immediate split across two fields. The narrow offset is sufficient for most stack accesses (local variables and spilled registers sit within a few hundred bytes of the stack pointer) and for structure field accesses (most structures are small). When the offset is insufficient, the compiler inserts an ADDI to adjust the base first.

AArch64 base + offset

AArch64 load and store instructions accept a 12-bit unsigned immediate that is scaled by the access size: the offset is multiplied by 4 for 32-bit loads, by 8 for 64-bit loads. The scaling extends the reach to 16 KB above the base register for word loads and 32 KB above it for double-word loads without widening the offset field. Because the field is unsigned, this form cannot express a negative offset. An alternative encoding accepts a 9-bit signed unscaled offset, which covers negative displacements and byte-granularity access.

x86-64 base + displacement

x86-64 encodes the base register in the ModR/M r/m field and the displacement as 0, 8, or 32 bits selected by the mod field. The 8-bit displacement covers offsets of −128-128 to +127+127, sufficient for most stack-frame accesses. The 32-bit displacement covers ±\pm2 GB. The variable displacement width means that instructions accessing nearby data are shorter than those accessing distant data, improving average code density.

Three different answers to the same question

The three subsections above describe three ways of spending a handful of encoding bits on reach, and the differences are easier to judge when the ranges are drawn to scale rather than quoted as numbers. Figure 2 does that, and marks on the same axis the three data structures the offsets actually have to reach.

Figure 2
Figure 2. Offset reach on a logarithmic axis, with the structures the offsets must cover marked above. Each ISA covers the stack frame, which is the case that dominates. They diverge on everything larger, and each divergence is a different bet about how often that case occurs.

Read that way, the three designs are answering the same question with three different guesses about the distribution.

RISC-V spent twelve bits and made them signed, which covers a stack frame in both directions from a frame pointer and stops there. The bet is that anything larger is rare enough to pay two instructions for, and that keeping every I-type immediate identical is worth more than reach. The uniformity is real: the same twelve bits in the same position mean the same thing in addi, lw, jalr and every other I-type instruction, so the sign-extension hardware is one block used by everything.

AArch64 spent the same twelve bits and scaled them, buying four times the reach for free. The cost is that a scaled offset cannot address an unaligned location, and that the field is unsigned, so negative offsets need a different encoding entirely. AArch64 therefore has two base-plus-offset forms rather than one, which is exactly the uniformity RISC-V declined to give up.

x86-64 refused to choose and encodes the width in the instruction. The 8-bit form is one byte and covers the stack frame; the 32-bit form is four bytes and covers essentially everything. Programs pay for reach only where they use it, which is the strongest form of the code-density argument, and they pay for it with a decoder that cannot know an instruction’s length until it has read the ModR/M byte.

05.Indexed and Scaled-Indexed Addressing

Indexed addressing adds a second register to the address computation: EA=base+index\text{EA} = \text{base} + \text{index}. When the index register is multiplied by a constant scale factor, the mode becomes scaled-indexed: EA=base+index×scale\text{EA} = \text{base} + \text{index} \times \text{scale}.

x86-64 SIB addressing

The fullest expression of scaled-indexed addressing in any mainstream ISA is the x86-64 SIB byte, which encodes base + index ×\times scale + displacement with scale ∈{1,2,4,8}\in \{1, 2, 4, 8\} (the equation above in Chapter 17). This mode maps directly to C array access: for an array of 8-byte elements at base address arr, the element at index i lives at arr + i * 8. The SIB byte eliminates the shift instruction that RISC-V would need to scale the index.

AArch64 register-offset addressing

AArch64 supports a register-offset mode: LDR X0, [X1, X2, LSL #3], where X2 is shifted left by the log2_2 of the access size before being added to X1. This achieves the same effect as x86-64’s SIB addressing but through the barrel shifter on the address computation path. The shift amount is restricted to 0 or the log2_2 of the element size (0, 1, 2, or 3 for byte, halfword, word, and doubleword), which covers the common array-access case without a fully general scale factor.

AArch64 also supports an SXTW (sign-extend word) or UXTW (zero-extend word) modifier on the index register, allowing a 32-bit array index to be used directly as the offset without a separate sign-extension instruction.

RISC-V: no indexed addressing

RISC-V deliberately omits indexed addressing from the base integer ISA. The only load/store address form is base + 12-bit immediate. To access arr[i], the compiler must compute base + i * element_size in a separate instruction sequence (typically a shift followed by an add) and then use the result as the base register for a load with offset zero.

This design simplifies the address-generation hardware: the AGU needs only one adder (register + sign-extended immediate), not a second register read port and a shifter. The tradeoff is a higher dynamic instruction count for array-intensive code. The RISC-V architects judged that the hardware simplification was worth the extra instructions, especially in area-constrained embedded cores where every multiplexer and read port costs power [1].

06.PC-Relative Addressing

PC-relative addressing computes the effective address as the program counter plus a signed displacement. Its primary use is position-independent code (PIC): if every data and code reference is expressed relative to the current instruction’s address, the entire program can be loaded at any base address without relocation fixups.

Why this stopped being optional

PC-relative data addressing was a convenience for most of computing history and became mandatory over roughly a decade, for reasons that have nothing to do with instruction sets and everything to do with how programs are loaded. It is worth understanding the pressure, because it explains why all three ISAs acquired a dedicated mechanism for it and why they acquired different ones.

Shared libraries came first. A library mapped into many processes must occupy one physical copy, which means its code pages must be identical in every process, which means the code cannot contain absolute addresses patched per process. If the code refers to its own data relative to the program counter, the same bytes work at any load address, and one physical page serves everyone.

Address-space layout randomization made it universal. Placing the executable, the stack, the heap and each library at a different random base on every run is one of the cheapest defenses against memory-corruption exploits, because it denies the attacker the addresses their payload needs. It requires that the program work at any base, which is the same requirement, now applied to the main executable and not just to libraries. Position-independent executables became the default across Linux distributions, macOS and Windows within a few years of each other.

The result is that the most common form of data access in a modern binary, reading a global, is a PC-relative access. An addressing mode that was a specialty feature is now on the hot path, and Figure 3 shows the three answers.

Figure 3
Figure 3. Position-independent access to a global on the three ISAs. The fixed-width machines need two instructions because a 32-bit instruction cannot contain a 32-bit displacement and an opcode. The variable-length machine needs one, because it simply appends four more bytes.

This is the single clearest case in the book where variable-length encoding wins outright, and it is worth admitting plainly rather than explaining away. x86-64 added RIP-relative addressing in the 64-bit extension, at a moment when it could choose freely, and it got a one-instruction global access because appending a displacement costs it nothing structural. RISC-V and AArch64 cannot have that at any price, because the constraint is arithmetic: a 32-bit instruction cannot hold a 32-bit displacement plus an opcode.

The fixed-width machines mitigate rather than solve. RISC-V’s AUIPC computes the high twenty bits of a PC-relative address into a register, leaving the low twelve to the load that follows, and the linker relaxes the pair into something shorter when the target turns out to be close. AArch64’s ADRP is the same idea with the split chosen at a page boundary, which is why it reaches four gigabytes while producing only a page address. In both cases the second instruction was going to be a load anyway, so the true cost is one extra instruction rather than two.

RISC-V AUIPC and PC-relative branches

RISC-V provides AUIPC (Add Upper Immediate to PC), which loads the PC plus a 20-bit upper immediate into a register. The compiler then adds a 12-bit offset with ADDI or LW to reach any address within ±\pm2 GB of the current instruction. Branch instructions (B-type) encode a 13-bit signed offset (12 bits shifted left by 1, giving 2-byte alignment), and JAL encodes a 21-bit signed offset.

AArch64 ADRP and ADR

AArch64 provides ADRP (form PC-relative address to 4 KB page) and ADR (form PC-relative address with 21-bit offset). The typical pattern for a global-variable access is ADRP X0, symbol followed by LDR X1, [X0, #:lo12:symbol], reaching any address within ±\pm4 GB of the current PC.

x86-64 RIP-relative addressing

x86-64 made RIP-relative addressing the default for memory operands in 64-bit mode. When the ModR/M byte has mod=00 and r/m=101, the effective address is RIP plus a 32-bit signed displacement. The System V AMD64 ABI requires position-independent code in shared libraries, and RIP-relative addressing makes this efficient: every global data reference is a single instruction with no relocation at load time. The 32-bit displacement limits the reachable range to ±\pm2 GB from the instruction, which is sufficient for most executables linked with the small or medium code models.

07.ARM Pre-Indexed and Post-Indexed Addressing

AArch64 provides two addressing modes that update the base register as a side effect of the load or store. Neither RISC-V nor x86-64 offers an equivalent.

Pre-indexed addressing

In pre-indexed mode, the base register is updated to the computed address before (or simultaneously with) the memory access:

AArch64 pre-indexed load: X1 is updated to X1 + 16, then the doubleword at the new X1 is loaded into X0.

Code
LDR X0, [X1, #16]! // X1 = X1 + 16; X0 = Mem[X1]

The exclamation mark in the assembly syntax signals the writeback. Pre-indexed mode is useful for walking through a data structure where each access advances the pointer: the load and the pointer update happen in a single instruction, reducing the dynamic instruction count.

Post-indexed addressing

In post-indexed mode, the memory access uses the original base register value, and the base register is updated afterward:

AArch64 post-indexed store: the doubleword in X0 is stored at the current X1, then X1 is incremented by 8.

Code
STR X0, [X1], #8 // Mem[X1] = X0; X1 = X1 + 8

Post-indexed mode is the natural fit for a loop that processes elements in order: the store writes to the current position, and the pointer advances to the next position in the same instruction.

Both pre-indexed and post-indexed modes require the base register to have a write port in the same cycle as the load or store data path. RISC-V avoids this complexity by keeping load and store semantics simple and letting the compiler emit a separate addi for the pointer update.

08.The Barrel-Shifter Operand

AArch64 arithmetic and logical instructions can apply a shift or extension to the second source operand before it enters the ALU. This barrel-shifter operand is a distinctive feature inherited from the original ARM architecture (ARMv1, 1985).

For example, the instruction ADD X0, X1, X2, LSL #3 computes X0=X1+(X2≪3)\texttt{X0} = \texttt{X1} + (\texttt{X2} \ll 3). The available shift operations are LSL (logical shift left), LSR (logical shift right), ASR (arithmetic shift right), and ROR (rotate right). The shift amount is a 6-bit unsigned immediate (0–63 for 64-bit operations).

The barrel shifter adds no extra cycle because the shift is integrated into the ALU’s input multiplexer. The hardware cost is a combinational shifter on one of the ALU input paths, which is modest in area but occupies a non-trivial fraction of the ALU’s critical-path delay. RISC-V and x86-64 do not provide an equivalent: shifting a source operand requires a separate shift instruction.

The barrel-shifter operand is especially valuable for address arithmetic. Computing base + index * 8 is a single ADD with LSL #3 on AArch64, two instructions (shift then add) on RISC-V, and one instruction (using the SIB byte) on x86-64. For general-purpose scaling by powers of two, the barrel-shifter approach and the SIB-byte approach are different hardware answers to the same compiler need.

Why a free operand is not free

The sentence above that the barrel shifter “adds no extra cycle” is the standard description and it deserves unpacking, because it is true in a specific and limited sense that matters for how these decisions get made.

The shifter adds no extra cycle. It does add delay. A 64-bit barrel shifter is a network of six multiplexer stages, one per bit of shift amount, and each stage costs a gate delay or two. Placing it on an ALU input path lengthens the combinational path from register read to result by roughly the delay of those six stages. Whether that costs a cycle depends entirely on how much slack the cycle had.

That is the actual trade, and it is a floorplanning trade rather than an instruction-set one. If the ALU path already has slack, the shifter is genuinely free and AArch64 gets a fused shift-and-add for nothing. If the ALU path is the critical path that sets the clock frequency, which it very often is, then the shifter’s delay subtracts from every instruction’s timing budget, and the whole machine runs slower to make one instruction form faster.

Three consequences follow, and they explain the divergence better than any argument about RISC purity.

The shifted operand is not always one cycle even on AArch64. Implementations that cannot absorb the delay handle the shifted forms at reduced throughput or crack them into two micro-ops. The ISA promises the instruction exists, not that it is free, and optimization guides for specific cores document which shift amounts are full-rate.

x86-64’s SIB byte avoids the problem entirely by restricting the shift to a scale of 1, 2, 4 or 8. Two bits of scale select among four fixed shifts, which is a four-input multiplexer rather than a six-stage network, and it sits in the address generation unit rather than the ALU. Addressing needed exactly the power-of-two scaling that array indexing produces, and nothing more, so the narrower mechanism loses nothing real.

RISC-V’s answer arrived later and looks like x86’s. The base ISA has neither, and the ratified Zba extension adds sh1add, sh2add and sh3add, which shift by exactly one, two or three and add. That is the SIB byte’s scale set, reached from the opposite direction: not a general shifter made cheap, but the three shifts that matter given their own opcodes.

09.Comparison Table

Table 1. Addressing mode support across three ISAs. A check mark indicates native hardware support. A dash indicates that the mode is not available in the base integer ISA.

Addressing ModeRISC-VAArch64x86-64
Immediate✓\checkmark✓\checkmark✓\checkmark
Register✓\checkmark✓\checkmark✓\checkmark
Base + displacement✓\checkmark✓\checkmark✓\checkmark
Base + index—✓\checkmark✓\checkmark
Base + scaled index—✓\checkmark✓\checkmark
Base + scaled index + disp.——✓\checkmark
PC-relative (data)✓\checkmark✓\checkmark✓\checkmark
Pre-indexed—✓\checkmark—
Post-indexed—✓\checkmark—
Barrel-shifted operand—✓\checkmark—

The table tells a clear story. RISC-V occupies the minimal end of the spectrum, with a single load/store addressing mode, base plus a 12-bit signed displacement. AArch64 occupies the middle ground, adding indexed modes and the barrel-shifter operand while keeping the encoding fixed-length. x86-64 occupies the maximal end, with the richest address computation available in a single instruction, at the cost of variable-length encoding and decoder complexity.

10.Encoding Side-Effects: Width Limits and Hardware Cost

The choice of addressing modes has consequences that ripple through the hardware. This section highlights three encoding side-effects that affect compiler writers and microarchitects.

Immediate field width and constant materialization

RISC-V’s 12-bit immediate means that any constant outside the range −2048-2048 to +2047+2047 requires a LUI + ADDI sequence. AArch64’s 12-bit unsigned arithmetic immediate (with optional 12-bit shift) covers the range 0 to 16,773,120 for page-aligned constants, and the bitmask immediate handles a different set of useful constants. x86-64’s 32-bit immediate covers almost any constant a compiler would generate for a single instruction.

The practical impact is that RISC-V code for constant-heavy workloads (hash functions, cryptographic primitives, lookup tables of magic numbers) tends to have a higher instruction count than the same algorithm on x86-64 or AArch64.

Address-generation unit complexity

On RISC-V, the AGU is a single adder: register + sign-extended 12-bit immediate. On AArch64, the AGU must also handle register + register (with optional shift), pre-indexed writeback, and post-indexed writeback. On x86-64, the AGU must handle register + register ×\times scale + displacement, which requires a shifter (for the scale) and two adders (or a three-input adder). The AGU complexity directly affects the pipeline’s cycle time and the area budget for the load-store unit.

Compiler register pressure

Addressing modes that fold more computation into the load or store instruction reduce the number of “setup” instructions the compiler must emit and free registers for other uses. The x86-64 SIB addressing mode, for example, lets the compiler keep a base pointer in one register and an index in another without needing a third register to hold the scaled address. On RISC-V, the compiler must compute the scaled address in a temporary register, which consumes one of the 32 GPRs (31 usable, since x0 is hardwired to zero). The practical difference is small for most code, but it becomes visible in tight inner loops with many live variables.

11.Why RISC-V Has Fewer Modes

The RISC-V ISA manual [1] explains the rationale directly: every addressing mode beyond base + offset adds a read port to the register file (for the index register), a multiplexer in the address path, and a writeback hazard path (for pre/post-indexed modes). In an area-constrained embedded core running at a few hundred megahertz, these additions are proportionally expensive. The RISC-V architects chose the smallest mode set that a C compiler can target without heroic effort, and they left richer address computation to explicit arithmetic instructions.

The tradeoff is measurable. Studies comparing dynamic instruction counts on the SPEC CPU benchmark suite show that RISC-V code is typically 10 to 25 percent larger in instruction count than AArch64 code for the same workload, with much of the difference coming from address computation overhead and constant materialization. The code-size gap is smaller than the instruction-count gap because RISC-V’s fixed 32-bit encoding (or 16-bit with the C extension) produces compact binaries.

For high-performance out-of-order cores, the extra instructions are largely hidden by the execution engine’s ability to issue multiple instructions per cycle. For in-order cores with narrow pipelines, the extra instructions translate directly to extra cycles. The RISC-V vector extension (Chapter 21) and the proposed “B” bit-manipulation extension partially close the gap by folding common address patterns into vector-load strides and bit-field operations.

12.Worked Examples

13.Exercises

References

  1. [1]Waterman, Andrew and Asanovi\'c (2024). “The RISC-V.”
FeedbackBook mode
computer-architectureinstruction-set-architectures