Part 1 · The register machine
`lea` is arithmetic
The most misread instruction in x86-64. It has brackets, its name says "load", and it never touches memory.
What this lesson is trying to break (3)
- `lea` loads from memory.
- Brackets in an operand always mean a memory access.
- Array indexing needs a separate multiply.
Start with the contrast
Two instructions, near-identical text, completely different behaviour.
Varies: only the mnemonic — the bracketed operand is identical
| Instruction | What happens | Touches memory? | Result in rax |
|---|---|---|---|
mov rax, [rdi+8] | Read 8 bytes from address rdi+8 | Yes | the value stored there |
lea rax, [rdi+8] | Compute rdi+8 | No | the number rdi+8 |
Same brackets, opposite meanings. The brackets describe an address computation; the mnemonic decides whether that address is then used.
So: brackets mean “an address is being computed here”. They do not mean an
access happens. lea computes and stops.
Why compilers love it
x86-64’s addressing modes can encode a surprising amount in one operand:
[ base + index*scale + displacement ]
│ │ │ │
│ │ │ └─ any 32-bit constant
│ │ └────────── 1, 2, 4 or 8 only
│ └───────────────── any register
└───────────────────────── any register
The scale values are exactly the sizes of char, short, int/float and
long/double/pointer — because the whole mechanism exists to make array
indexing free. arr[i] for int arr[] is [rdi + rsi*4], one operand, no
multiply instruction.
And because lea exposes that hardware without the access, the compiler gets a
free three-input adder with a built-in shift.
int t3(int x) {
return x * 3;
}Two:
"t3":
lea eax, [rdi+rdi*2]
retrdi + rdi*2 is 3 × rdi. There is no imul, no shift, no add — one
address-generation operation does the whole multiplication, and nothing is
loaded.
Try x * 5 ([rdi+rdi*4]) and x * 9 ([rdi+rdi*8]) — all single leas.
Then try x * 7 and watch it become something else, because 7 is not
1 + 2ⁿ for any allowed scale. That boundary is exactly where the trick stops
working, and finding it yourself is worth more than being told.
lea rax, [rdi+rdi*2] · lea rax, [rax+rax*4]
A multiplication by a small constant. Nothing is being loaded.
The pattern [r + r*n] computes r × (n+1), so scales 1, 2, 4, 8 give free
multiplies by 2, 3, 5 and 9.
You will also see lea used for plain addition when the compiler wants to keep
both inputs intact — lea eax, [rdi+rsi] is eax = rdi + rsi without destroying
either, which two-operand add cannot do. That is lea compensating for x86’s
missing three-operand arithmetic, and it is why ARM64 needs no equivalent trick.
What ARM64 does instead
ARM64 has no lea, and does not need one, for two reasons.
Its arithmetic is already three-operand, so add w0, w1, w2 keeps both inputs —
the main non-multiply use of lea disappears.
And it can shift an operand as part of the instruction:
add x0, x1, x2, lsl 3 ; x0 = x1 + (x2 << 3)
That covers base + index*8 directly in an ordinary add. So the capability
transfers exactly; only the spelling differs. Compile x * 3 for ARM64 and you
will see it use an add-with-shift or a plain mul — either way, no bracketed
operand, because ARM64 reserves brackets strictly for real memory access.
This is the recurring lesson of Part 1 in one instruction: the capability is essential, the syntax is incidental, and a single-ISA reader cannot tell which is which.
Someone reviewing a disassembly says:
“There are five memory accesses in this loop — look at all the brackets.”
They have counted three leas among them. Write the correction you would give,
and add the one thing they should look at instead to count real accesses.
What you should now be able to say
- Brackets mean address computation, not access. The mnemonic decides.
leagives youbase + index*scale + displacementas pure arithmetic.- Scale is 1, 2, 4 or 8 — the element sizes — which is why array indexing costs no separate multiply.
[r + r*n]is a free multiply by n+1; 2, 3, 5 and 9 are the reachable ones.leaalso substitutes for the three-operand add x86 lacks.- ARM64 achieves the same things with three-operand arithmetic and shifted register operands, and keeps brackets for real accesses only.