Introduction#
Consider this simple C program:
int global;
void update(void)
{
global = 42;
}At the source level, this looks trivial. Store the value 42 into a variable.
But the generated machine code reveals a fundamental difference between two popular instruction set architectures:
- x86-64, the dominant ISA in desktops and servers
- AArch64, the 64-bit ARM ISA used in phones, laptops, and increasingly servers
A typical x86-64 compiler output might look like:
movl $42, global(%rip)AArch64 might produce something like:
adrp x0, global
mov w1, #42
str w1, [x0, #:lo12:global]Both programs do the same thing. Both eventually write 42 into the same memory location.
So why does x86-64 appear to do everything in one instruction while AArch64 needs multiple instructions?
The answer is not that one ISA is more powerful. The difference comes from how they choose to encode instructions.
Position-independent code: the address is not known yet#
Modern operating systems usually load programs at different virtual addresses. This is useful for security features such as Address Space Layout Randomization (ASLR), as well as to support shared object libraries. The final address in the memory is unknown until these programs are loaded by the operating system.
Because of this, the compiler cannot simply encode: global = 42 into the machine code like mov [some_address], 42, as the final address of global is only known when the program is loaded.
Instead, both architectures use PC-relative addressing, where the CPU calculates how far a variable is from the address of the current instruction. This distance is constant no matter where the program is loaded in the memory.

How much information can each ISA fit into its instruction encoding to describe that offset?
x86-64: rich instructions with room for addresses#
x86-64 inherited the CISC philosophy of providing powerful instructions with many addressing modes. An x86-64 instruction can have a variable length, ranging from 1 byte upto 15 bytes. This gives the ISA room to encode more information.
For example:
movl $42, global(%rip)contains:
- The operation (
mov) - The value (
42) - A displacement relative to rip (the current instruction)
Since the instruction can encode up to 15 bytes, it can enclose a 4-bytes displacement, giving the program access to +/- 2GB from the current instruction.
For most executables, this is enough to reach nearby code and data.
The CPU can therefore calculate the address of global as the offset encoded into the instruction, plus the value of the rip register (instruction counter), then perform the store in one instruction.
The address calculation is hidden inside the instruction.
AArch64: fixed-size instructions#
AArch64 takes a different approach.
Every instruction is exactly 32 bits, no instruction is longer. no instruction is shorter.
This makes instruction decoding much simpler, but it also means every instruction has a strict space budget.
Those 32 bits need to contain things like:
- What operation to perform
- Which registers to use
- Immediate values
- other control bits
There is simply not enough room for a full 32-bit displacement as in x86-64
So instead of putting the entire address into one instruction, AArch64 builds it in pieces.
Building an address with ADRP#
The first instruction is:
adrp x0, globalADRP means:
Add the page-relative address of a symbol to the program counter.
This instruction conceptually views the memory as 4KB pages, so the instruction says: calculate how many pages are there between the page that contains the current instruction’s address, and the page that contains global. Store this value, multiplied by 4KB (simply 12-bits shifted left) in x0
Let’s say this number was found to be 3 pages, and the current instruction’s address is 0x1040, so x0 will hold 0x1000 + 3 * 0x1000 = 0x4000. This leaves us at the beginning of the page in which global lives.
We still need to add an offset to represent how many extra bytes do we need to move on from the beginning of that page’s address to exactly reach global.
This is what the third instruction does (the second instruction only moves 42 into some other register to use it in the store instruction).
str w1, [x0, #:lo12:global]This instruction says: add the lowest 12-bits of the full address of global to x0 (which already contains the address of the page that contains global. Lowest 12 bits exactly represent the offset within the 4KB pages.

Quick explanation if it still doesn’t make sense for you
Imagine 8-bits addressing. If we split it into 4 bytes pages, so each page needs 2 bits to address 4 bytes. Addresses in the same page share the same highest 6-bits that represent the page number, while they differ in the lowest 2-bits, which represent the exact byte offset within that page. The same applies here, but with larger number of bits: 4KB page means 12 lower bits, and number of pages is encoded in the highest 21-bits (total is 32 bits).
Now, AArch has the complete absolute address of global in x0, and hence can execute store operation successfully.
CISC vs RISC: where does the complexity go?#
It is tempting to summarize this as CISC uses fewer instructions, RISC uses more instructions. Despite true, this is too simplistic, as the real difference is that x86-64 puts more complexity into individual instructions to make programs easier, while AArch64 keeps instructions simpler and more regular, but programs have to take over some complexity.
x86-64 provides variable-length instructions, allowing more flexibility to encode memory operands, complex addressing modes and large encoded offsets.
On the other hand, AArch64 provides fixed-width instructions, simple decoding, explicit address construction and regular instruction formats
Both approaches are valid, but the flexibility of x86-64 does not come for free. As usual, there are always some trade-offs:
The cost of variable-length instructions#
Because instructions have different sizes, the processor executing x86 instructions first has to answer:
Where does each instruction begin and end?
The CPU cannot simply fetch a block and split it every four bytes, as AArch64 would naturally do.
Instead, the frontend needs additional complex instruction decoders hardware to detect instruction boundaries. This brings more transistors, more silicon area and more power consumption.
AArch64 chooses to simplify instruction’s design, resulting in simpler hardware decoder, fewer logic gates and transistor count, smaller silicon area and less power consumption. However, the compiler design would take over this complexity, as shown in the beginning of this blog post: one x86-64 instruction is equivalent to three AArch64 instructions.
This does not mean x86 processors are inefficient. Modern x86 CPUs use techniques such as micro-operation caches and sophisticated prediction mechanisms to reduce these costs.
The tradeoff is architectural:
- x86 spends more hardware complexity supporting a richer instruction set.
- AArch64 favours simple instructions and less power consumption, but more instructions expressing the same operations.
The bigger picture#
A single assignment statement looks identical in C:
global = 42;But the ISA underneath makes different choices.
x86-64 hides more work inside individual instructions.
AArch64 exposes more of that work as explicit instructions.
Neither architecture is “better.” They simply place complexity in different places.
The result is two different ways of solving the same problem, using different ISAs, thinking in two different philosophies.
Reply by Email