Assembly (x86-64, AArch64, RISC-V) Module 19 – The three ISAs today, compared
Comparing x86-64, AArch64 and RISC-V
Compare x86-64, AArch64 and RISC-V on registers, encodings, flags, division and memory order, with the code size of five C functions measured.
What you will learn
- Compare x86-64, AArch64 and RISC-V on registers, instruction lengths, flags, division by zero, memory order and system calls
- Measure the bytes and instructions of the same C functions on four targets and explain the differences
- Explain why micro-operations and instruction fusion make an ISA an interface rather than a microarchitecture
- Count what one block of a hand-written loop costs on each ISA
- Choose an ISA for a job and name the porting differences that change a program's results
Before you start
What you’ll learn · Before you start
What you will learn
- Compare x86-64, AArch64 and RISC-V on registers, instruction lengths, flags, division by zero, memory order and system calls
- Measure the bytes and instructions of the same C functions on four targets and explain the differences
- Explain why micro-operations and instruction fusion make an ISA an interface rather than a microarchitecture
- Count what one block of a hand-written loop costs on each ISA
- Choose an ISA for a job and name the porting differences that change a program's results
Before you start
On this page
Compiled by one compiler for size, five small C functions take 127 bytes of x86-64 code, 176 of AArch64 code and 122 of RISC-V code, or 188 when RISC-V leaves out its compressed instructions. Comparing x86-64 with the two load/store ISAs on measured code beats any slogan: what decides is which operations each encoding makes short.
This page compares the three ISAs in one table and measures those five functions on four targets. Three further parts show what modelled cores make of an instruction count, write one base64 encoder by hand for each ISA, and port C between the ISAs.
Seven rows hold the differences you meet first
Each of the three is an instruction set architecture: The contract between software and a processor: the registers a program may name, the instructions that act on them, how those instructions are encoded as bytes and the rules for memory, interrupts and privilege. x86-64, AArch64 and RISC-V are three ISAs. More in “What assembly language is and why it matters”, a contract for registers, instructions, their bytes and memory. Comparing x86-64, AArch64 and RISC-V row by row shows where the contracts differ:
| x86-64 | AArch64 | RISC-V (RV64) | |
|---|---|---|---|
| General-purpose registers | 16, or 32 with APX | 31, plus sp and a zero register | 31, plus x0, which reads as 0 |
| Instruction length | 1 to 15 bytes | always 4 bytes | 4 bytes, or 2 with C |
| Condition flags | RFLAGS, set by most arithmetic | NZCV, set only on request | none |
| Memory operands | most arithmetic can take one | loads and stores only | loads and stores only |
| Integer division by zero | divide-error exception | result 0 | every bit of the quotient set |
| Memory order | total store order | weakly ordered | RVWMO, weakly ordered |
| Linux write system call | syscall, rax = 1 |
svc #0, x8 = 64 |
ecall, a7 = 64 |
x86-64 lets most arithmetic read or write memory and packs it into
variable-length encoding: An instruction encoding in which an instruction's size depends on what it does. x86-64 instructions are 1 to 15 bytes of optional prefixes, opcode, ModRM, SIB, displacement and immediate, which packs code densely but complicates decoding. More in “Instruction encoding: fixed and variable”. It sets its
condition flags: Single bits that an instruction sets to describe its result: zero, negative, carry and signed overflow. x86-64 keeps them in RFLAGS as ZF, SF, CF and OF, AArch64 in NZCV; RISC-V has none. More in “Flags and condition codes” as a side effect of almost every add and compare. AArch64 is a
load/store architecture: An instruction set in which only load and store instructions access memory and every arithmetic or logic instruction works on registers alone. AArch64 and RISC-V are load/store; x86-64 is not, because many of its instructions take a memory operand. More in “Loads and stores of every width” with a fixed 4-byte encoding. Its NZCV flags change
only when an instruction such as cmp asks. RISC-V is load/store too and has no flags: blt a0, a1, label compares
two registers itself. Its C extension adds compressed instructions: Two-byte encodings of common RISC-V instructions, defined by the C extension. Each one expands to a single standard 4-byte instruction, so the same program takes less space; a core must implement C to run them. More in “Compiler idioms you will meet”, 2-byte
forms of the commonest ones.
Five everyday operations show the rows at work:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Add two registers. x86-64 add rax, rcx takes 3 bytes and overwrites rax with the sum. AArch64 add x0, x0, x1 takes 4 bytes, like every A64 instruction, and could name a third register as the destination. RISC-V add a0, a0, a1 fits the 2-byte c.add form of the C extension.
- Load a 64-bit constant. For the constant 0xcbf29ce484222325, x86-64 has one movabs of 10 bytes. AArch64 builds it 16 bits at a time with mov and three movk, 16 bytes. The RISC-V compiler loads it from a constant pool with auipc and ld: 8 bytes of code and 8 bytes of data.
- Add 1 to a counter in memory. x86-64 does it in one instruction with a memory operand, inc dword ptr [rdi + 4*rax + 8], 4 bytes. AArch64 and RISC-V are load/store ISAs, so the work is a load, an add and a store: 12 bytes on AArch64, 6 bytes of compressed forms on RISC-V.
- Divide by zero. x86-64 div and idiv raise the divide-error exception, which Linux delivers as SIGFPE. AArch64 udiv and sdiv write 0 and carry on. RISC-V divu writes a value with every bit set, and remu returns the dividend.
- Ask Linux to write. The system call write is number 1 on x86-64, passed in rax, and the instruction is syscall. AArch64 and RISC-V share Linux's generic table, where write is 64, passed in x8 with svc #0 and in a7 with ecall.
The division and system-call rows change behaviour after a port. Division runs all three division-by-zero cases, and Linux system calls shows why AArch64 and RISC-V share one table of system-call number: The number a program loads into a register (rax, x8 or a7 on Linux) to say which system call it wants. Numbers come from a per-architecture table, so write is 1 on x86-64 and 64 on AArch64. More in “Linux system calls on three ISAs”. Under total store order, other cores see a core’s stores in program order. The weak models keep that order only where code asks, as memory ordering shows.
Checkpoint
Check: Where the three ISAs differ
Code size follows the encodings, not the CISC or RISC label
The usual story says a CISC: Complex instruction set computer: a label for ISAs such as x86-64 whose instructions vary in length and whose arithmetic instructions can read or write memory directly. instruction set gives smaller programs than a
RISC: Reduced instruction set computer: a label for load/store ISAs such as AArch64 and RISC-V, whose regular instructions touch memory only through loads and stores. one because each instruction does more. five.c tests it with five functions that stress different parts of an
encoding: a division loop, two comparisons, a byte loop, two 64-bit constants and two counters in memory.
/* five.c: five small functions, compiled for four targets. */
#include <stddef.h>
#include <stdint.h>
/* Euclid's algorithm: a loop around one division. */
uint64_t gcd(uint64_t a, uint64_t b) {
while (b != 0) {
uint64_t r = a % b;
a = b;
b = r;
}
return a;
}
/* Two comparisons and no loop. */
int clamp(int x, int lo, int hi) {
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
/* A byte loop that stops at the first byte that is not a space. */
size_t skip_spaces(const char *s) {
size_t n = 0;
while (s[n] == ' ') n++;
return n;
}
/* FNV-1a: two 64-bit constants and a multiply per byte. */
uint64_t fnv1a(const unsigned char *p, size_t n) {
uint64_t h = 0xcbf29ce484222325u;
for (size_t i = 0; i < n; i++) {
h ^= p[i];
h *= 0x100000001b3u;
}
return h;
}
/* Two counters in memory, each read, increased and written back. */
struct stats { long total; int counts[64]; };
void bump(struct stats *s, int i) {
s->counts[i]++;
s->total++;
} The session compiles it with LLVM’s clang for x86-64, AArch64, RV64GC and RV64G (RISC-V without C), for size (-Os)
and for speed (-O2), then reads each function’s bytes from the symbol table and counts its instructions:
"""Compiles five.c for four targets, then counts the bytes and the instructions of each function."""
import collections
import os
import pathlib
import re
import subprocess
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
TARGETS = {
"x86-64": "--target=x86_64-linux-gnu",
"AArch64": "--target=aarch64-linux-gnu",
"RV64GC": "--target=riscv64-linux-gnu -march=rv64gc",
"RV64G": "--target=riscv64-linux-gnu -march=rv64g",
}
LEVELS = ("-Os", "-O2") # optimised for size, then for speed
session.run("\n".join(
[f"# The same file for four targets, optimised for size ({LEVELS[0]}), then for speed ({LEVELS[1]})"]
+ [f"clang-23 {flags} {level} -c five.c -o {name}{level}.o"
for level in LEVELS for name, flags in TARGETS.items()]
+ ["# x86-64 once more, with indirect-branch tracking turned on",
"clang-23 --target=x86_64-linux-gnu -Os -fcf-protection=full -c five.c -o x86-64-cet.o",
"# The RISC-V objects keep one 64-bit constant of fnv1a outside the code",
"llvm-size -A RV64GC-Os.o | grep -E '^[.](text|rodata)'"]
))
# The measuring tools are found the way the session above finds them.
ENV = dict(os.environ, PATH=os.pathsep.join([os.environ.get("PATH", ""), *session.EXTRA_BIN]))
def tool(*args: str) -> str:
return subprocess.run(args, capture_output=True, text=True, env=ENV, check=True).stdout
def functions(obj: str) -> list[str]:
"""The functions an object file defines, in address order, so a function added to five.c is measured too."""
names = []
for line in tool("llvm-nm", "-S", "--defined-only", "-n", obj).splitlines():
parts = line.split()
if len(parts) == 4 and parts[2] in ("T", "t"):
names.append(parts[3])
return names
FUNCTIONS = functions(f"x86-64{LEVELS[0]}.o")
def measure(obj: str) -> dict[str, list[tuple[int, str]]]:
"""Returns the length in bytes and the mnemonic of every instruction of each function in one object file."""
symbols = {}
for line in tool("llvm-nm", "-S", "--defined-only", obj).splitlines():
parts = line.split()
if len(parts) == 4 and parts[3] in FUNCTIONS:
symbols[parts[3]] = (int(parts[0], 16), int(parts[1], 16))
found = collections.defaultdict(list)
for line in tool("llvm-objdump", "-d", obj).splitlines():
match = re.match(r"\s+([0-9a-f]+):\s+([0-9a-f ]+?)\s*\t(\S+)", line)
if not match:
continue
address, size = int(match.group(1), 16), len(match.group(2).replace(" ", "")) // 2
for name, (start, length) in symbols.items():
if start <= address < start + length:
found[name].append((size, match.group(3)))
for name, (start, length) in symbols.items():
assert sum(size for size, _ in found[name]) == length, (obj, name) # every byte of the symbol was counted
return found
def total(insns: list[tuple[int, str]]) -> str:
return f"{sum(size for size, _ in insns)} / {len(insns)}"
print()
x86 = {}
for level in LEVELS:
results = {name: measure(f"{name}{level}.o") for name in TARGETS}
x86[level] = results["x86-64"]
print(f"Bytes / instructions per function at {level}")
print(f"{'function':<12}" + "".join(f"{name:>11}" for name in TARGETS))
for function in FUNCTIONS:
print(f"{function:<12}" + "".join(f"{total(r[function]):>11}" for r in results.values()))
totals = [total([insn for insns in r.values() for insn in insns]) for r in results.values()]
print(f"{'total':<12}" + "".join(f"{cell:>11}" for cell in totals))
if level == LEVELS[0]:
print(f"Instruction lengths at {level} (bytes: how many instructions)")
for name, r in results.items():
counts = collections.Counter(size for insns in r.values() for size, _ in insns)
print(f" {name:<8}", ", ".join(f"{size}: {n}" for size, n in sorted(counts.items())))
print()
print(f"Where x86-64's extra bytes come from ({LEVELS[0]}, then {LEVELS[1]})")
for label, function, prefix in [("imul in fnv1a", "fnv1a", "imul"), ("div in gcd", "gcd", "div")]:
counts = [sum(m.startswith(prefix) for _, m in x86[level].get(function, [])) for level in LEVELS]
print(f" {label + ':':<22}{counts[0]}, then {counts[1]}")
padding = [sum(size for insns in x86[level].values() for size, m in insns if m.startswith("nop")) for level in LEVELS]
print(f" {'bytes of nop padding:':<22}{padding[0]}, then {padding[1]}")
print()
cet = measure("x86-64-cet.o")
print("x86-64 at -Os with -fcf-protection=full:", sum(size for insns in cet.values() for size, _ in insns), "bytes /",
sum(map(len, cet.values())), "instructions")
print()
print("bump at -Os, instruction by instruction")
for name, flags in TARGETS.items():
syntax = ["-M", "intel"] if name == "x86-64" else []
listing = tool("llvm-objdump", "-d", "--no-addresses", *syntax, "--disassemble-symbols=bump", f"{name}-Os.o")
lines = [line.strip() for line in listing.splitlines() if line.startswith((" ", "\t")) and line.strip()]
print(f" {name}:")
for line in lines:
print(" " + re.sub(r"\s+", " ", line)) Output
# The same file for four targets, optimised for size (-Os), then for speed (-O2)
$ clang-23 --target=x86_64-linux-gnu -Os -c five.c -o x86-64-Os.o
$ clang-23 --target=aarch64-linux-gnu -Os -c five.c -o AArch64-Os.o
$ clang-23 --target=riscv64-linux-gnu -march=rv64gc -Os -c five.c -o RV64GC-Os.o
$ clang-23 --target=riscv64-linux-gnu -march=rv64g -Os -c five.c -o RV64G-Os.o
$ clang-23 --target=x86_64-linux-gnu -O2 -c five.c -o x86-64-O2.o
$ clang-23 --target=aarch64-linux-gnu -O2 -c five.c -o AArch64-O2.o
$ clang-23 --target=riscv64-linux-gnu -march=rv64gc -O2 -c five.c -o RV64GC-O2.o
$ clang-23 --target=riscv64-linux-gnu -march=rv64g -O2 -c five.c -o RV64G-O2.o
# x86-64 once more, with indirect-branch tracking turned on
$ clang-23 --target=x86_64-linux-gnu -Os -fcf-protection=full -c five.c -o x86-64-cet.o
# The RISC-V objects keep one 64-bit constant of fnv1a outside the code
$ llvm-size -A RV64GC-Os.o | grep -E '^[.](text|rodata)'
.text 122 0
.rodata.cst8 8 0
Bytes / instructions per function at -Os
function x86-64 AArch64 RV64GC RV64G
gcd 36 / 14 40 / 10 20 / 9 36 / 9
clamp 13 / 6 20 / 5 18 / 7 28 / 7
skip_spaces 19 / 5 28 / 7 26 / 8 32 / 8
fnv1a 48 / 12 56 / 14 40 / 14 56 / 14
bump 11 / 4 32 / 8 18 / 9 36 / 9
total 127 / 41 176 / 44 122 / 47 188 / 47
Instruction lengths at -Os (bytes: how many instructions)
x86-64 1: 5, 2: 11, 3: 17, 4: 3, 5: 2, 7: 1, 10: 2
AArch64 4: 44
RV64GC 2: 33, 4: 14
RV64G 4: 47
Bytes / instructions per function at -O2
function x86-64 AArch64 RV64GC RV64G
gcd 54 / 21 32 / 8 16 / 7 28 / 7
clamp 13 / 6 20 / 5 24 / 9 36 / 9
skip_spaces 28 / 6 28 / 7 26 / 8 32 / 8
fnv1a 165 / 40 56 / 14 40 / 14 56 / 14
bump 11 / 4 32 / 8 18 / 9 36 / 9
total 271 / 77 168 / 42 124 / 47 188 / 47
Where x86-64's extra bytes come from (-Os, then -O2)
imul in fnv1a: 1, then 5
div in gcd: 1, then 2
bytes of nop padding: 0, then 34
x86-64 at -Os with -fcf-protection=full: 147 bytes / 46 instructions
bump at -Os, instruction by instruction
x86-64:
48 63 c6 movsxd rax, esi
ff 44 87 08 inc dword ptr [rdi + 4*rax + 0x8]
48 ff 07 inc qword ptr [rdi]
c3 ret
AArch64:
8b21c808 add x8, x0, w1, sxtw #2
f940000a ldr x10, [x0]
b9400909 ldr w9, [x8, #0x8]
9100054a add x10, x10, #0x1
f900000a str x10, [x0]
11000529 add w9, w9, #0x1
b9000909 str w9, [x8, #0x8]
d65f03c0 ret
RV64GC:
058a slli a1, a1, 0x2
95aa add a1, a1, a0
6110 ld a2, 0x0(a0)
4594 lw a3, 0x8(a1)
0685 addi a3, a3, 0x1
0605 addi a2, a2, 0x1
e110 sd a2, 0x0(a0)
c594 sw a3, 0x8(a1)
8082 ret
RV64G:
00259593 slli a1, a1, 0x2
00b505b3 add a1, a0, a1
00053603 ld a2, 0x0(a0)
0085a683 lw a3, 0x8(a1)
00168693 addi a3, a3, 0x1
00160613 addi a2, a2, 0x1
00c53023 sd a2, 0x0(a0)
00d5a423 sw a3, 0x8(a1)
00008067 ret
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 size_session.py
At -Os, RV64GC is smallest with 122 bytes, then x86-64 with 127, AArch64 with 176 and RV64G with 188. Step through
the functions to see where the bytes go:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- gcd, a loop around a division. x86-64 div leaves the remainder in rdx. AArch64 has no remainder instruction, so it runs udiv and then msub. RISC-V remu returns the remainder directly, and with C the whole function takes 20 bytes.
- clamp, two comparisons. x86-64 compares and picks with cmovl twice, and AArch64 with cmp and csel twice. RV64GC has no conditional-select instruction, so it branches around register moves.
- skip_spaces, a byte loop. x86-64 compares the byte in memory with 0x20 in one cmp with a memory operand. AArch64 loads it first with ldrb. RISC-V must also add the index to the base, because its loads take only a register and an offset.
- fnv1a, two 64-bit constants. x86-64 spends 20 of its 48 bytes on two movabs instructions. AArch64 builds the constants with mov and movk. RISC-V builds the multiplier with three instructions and loads the other constant from 8 bytes of data.
- bump, two counters in memory. After one sign extension of i, two inc instructions with memory operands do the whole job on x86-64. AArch64 needs two loads, two adds and two stores. RISC-V also shifts the index apart, which AArch64's add folds in, yet all 9 of its instructions are 2-byte forms.
- The totals at -Os. RV64GC is smallest with 122 bytes, then x86-64 with 127, AArch64 with 176 and RV64G with 188. The same RISC-V instructions without the C extension take 54 % more space, so the 2-byte forms decide RISC-V's place.
- The totals at -O2. Optimising for speed changes x86-64 most: 271 bytes, because the compiler unrolls the fnv1a loop, adds a 32-bit fast path to the 64-bit division and pads loop starts with 34 bytes of nop. The other three targets change by 8 bytes or less.
Three effects explain the totals. Memory operands let x86-64 do in one instruction what a load/store ISA does in
three, so bump is 11 bytes there. Fixed 4-byte instructions cost AArch64 bytes, not instructions: it needs
fewer instructions than RISC-V in three of the five functions. RISC-V’s compressed forms make 33 of its 47
instructions 2 bytes long; without C the same instructions take 54 % more space.
The optimisation level moves the result more than the label does. At -O2, x86-64 grows to 271 bytes: the compiler
unrolls the fnv1a loop four times, adds a 32-bit fast path to the 64-bit division and pads loop starts with 34 bytes
of nop. The other targets change by 8 bytes or less. -fcf-protection=full adds a 4-byte endbr64 to each
function, 20 bytes for five, as hardware control-flow integrity
explains.
Key idea
Code size comes from the operations an encoding makes short, such as memory operands on x86-64 and 2-byte forms on RISC-V. Measure it with one source, one compiler and one set of options.
Checkpoint
Check: Measured code size on four targets
Practise
- Cover the first section’s table, write its seven rows from memory, then compare.
- Add a function to
five.cthat adds two 64-bit values in memory and stores the sum, predict the smallest target at-Os, then runsize_session.py. - Replace
-O2with-Oz, clang’s smallest-code level, insize_session.pyand compare the totals.
Practice that adapts to you
Practise this lesson
12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
What four modelled cores make of the same instructions Pro: opens with a Pro, Premium or Ultimate pass
An instruction count says what a program asks for, not what a core does. This part feeds one function to LLVM’s models of four cores, then lists the instruction pairs that cores of each ISA fuse.
In this part:
- One x86-64 instruction is 4 operations on one core and 1 on another
- Cores of all three ISAs fuse pairs of instructions
- What the counts can and cannot tell you
1 diagram · 1 runnable example (Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
One base64 encoder written by hand for each ISA, counted and run Pro: opens with a Pro, Premium or Ultimate pass
Compiled code shows what a compiler chooses; hand-written code shows what each ISA makes easy. This part writes the same base64 loop for x86-64, AArch64 and RISC-V, counts one block of each and runs the AArch64 version against Python.
In this part:
- The same block in three instruction sets
- What one block costs on each ISA
- Checking the AArch64 version against Python
5 runnable examples (ASM, C, Python, S) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Porting C between the ISAs, and choosing one for a job Pro: opens with a Pro, Premium or Ultimate pass
Rebuilding C for another ISA usually just works, and the places where it does not are known. This part reproduces the subtlest one, a floating-point result that changes with the target, lists three more with their numbers, and weighs which ISA fits a job.
In this part:
- One expression, two answers on four targets
- Three more differences that change results
- Choosing an ISA for a job
3 runnable examples (C, Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
How does each of x86-64, AArch64 and RISC-V decide a conditional branch?
x86-64 branches on RFLAGS, which most arithmetic sets: cmp rax, rcx then jl. AArch64 branches on NZCV, which only
flag-setting forms such as cmp and subs change, with b.lt, and cbz tests a register directly. RISC-V has no
flags: blt a0, a1, label compares two registers inside the branch, and slt writes 0 or 1 when the result is needed
as a value.
Three harder questions from the module’s bank: firmware that must shrink, a test that fails after a port, and an ISA for a device.
-
Mid to senior
Your firmware for a 64-bit RISC-V core is built for RV64GC at -Os and is 12 KB too big for its flash. The next revision of the core adds the Zcb extension. How would you estimate, before any board arrives, whether rebuilding for Zcb closes the gap, and what limits the saving?
-
Mid to senior
A numerical test passes on your x86-64 build servers and fails on AArch64, with the same source and the same compiler flags: one result differs in its last digits. What do you check first, and how do you make the two builds agree?
-
Senior to staff
You choose the ISA for a new network appliance. It runs a Linux distribution, your own C code and a vendor library shipped only as x86-64 binaries, and the team wants a custom instruction for a checksum. How do you decide?
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How do x86-64, AArch64 and RISC-V differ in instruction length, memory access and flags?
x86-64 has 1- to 15-byte instructions, memory operands and flags. AArch64 has 4-byte instructions, touches memory only with loads and stores, and sets flags on request. RISC-V has 4- or 2-byte instructions, loads and stores only, and no flags.
-
How does each ISA decide a conditional branch?
x86-64 branches on RFLAGS, which most arithmetic sets (cmp then jl). AArch64 branches on NZCV, which only flag-setting forms change (cmp then b.lt), and cbz tests a register directly. RISC-V compares two registers inside the branch itself, as blt a0, a1, label does.
-
Five C functions were compiled at -Os for four targets. What did the sizes show?
RV64GC needed 122 bytes, x86-64 127, AArch64 176 and RV64G 188: the compressed 2-byte forms mattered more than the CISC or RISC label.
-
Which changed code size more for the same functions, the ISA or the optimisation level?
The optimisation level: x86-64 grew from 127 bytes at -Os to 271 at -O2, more than the gap between any two ISAs at -Os.
-
How should code size be compared across ISAs?
With one source, one compiler and one set of options, because size comes from the operations each encoding makes short, such as memory operands on x86-64 and 2-byte forms on RISC-V.
Key takeaways
- x86-64 has 1- to 15-byte instructions, memory operands and flags; AArch64 has 4-byte instructions, loads and stores only and flags on request; RISC-V has 4- or 2-byte instructions and no flags.
- For five C functions at
-Os, RV64GC needed 122 bytes, x86-64 127, AArch64 176 and RV64G 188, so compressed forms mattered more than the CISC or RISC label. - The optimisation level changed code size more than the ISA did: x86-64 grew from 127 to 271 bytes at
-O2. - Division by zero traps on x86-64, returns 0 on AArch64 and sets every bit on RISC-V.
Terms in this lesson
- CISC
- Complex instruction set computer: a label for ISAs such as x86-64 whose instructions vary in length and whose arithmetic instructions can read or write memory directly.
- RISC
- Reduced instruction set computer: a label for load/store ISAs such as AArch64 and RISC-V, whose regular instructions touch memory only through loads and stores.
References
- Intel 64 and IA-32 Architectures Software Developer Manuals (instruction formats, DIV, memory ordering) (Intel Corporation)
- Arm A-profile A64 Instruction Set Architecture (UDIV, SDIV, UBFX, FMADD) (Arm Limited)
- The RISC-V Instruction Set Manual, Volume I: Introduction (standard, reserved and custom encodings) (RISC-V International)
- The RISC-V Instruction Set Manual, Volume I: RV32/64G instruction listings (custom-0 to custom-3) (RISC-V International)
- The RISC-V Instruction Set Manual, Volume I: "M" extension (division by zero, fusible sequences) (RISC-V International)
- The RISC-V Instruction Set Manual, Volume I: "C" extension for compressed instructions (RISC-V International)
- The RISC-V Instruction Set Manual, Volume I: RVWMO memory consistency model (RISC-V International)
- RVA23 Profiles (Zicclsm and misaligned accesses) (RISC-V International)
- System V AMD64 psABI (fundamental types: char is a signed byte) (x86-64 psABI maintainers)
- Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64): arithmetic types (Arm Limited)
- RISC-V ABIs Specification: calling convention (char is unsigned) (RISC-V International)
- syscall(2): calling conventions per architecture (The Linux man-pages project)
- RISC-V Hardware Probing Interface (misaligned access performance keys) (The kernel development community)
- llvm-mca: LLVM Machine Code Analyzer (LLVM Project)
- Clang Compiler User's Manual: -ffp-contract (LLVM Project)
- Clang command line argument reference: -fcf-protection (LLVM Project)
- N3220, working draft of ISO/IEC 9899:2024 (C23), 7.12.2 The FP_CONTRACT pragma (ISO/IEC JTC1/SC22/WG14)
- Python math module: math.fma (Python Software Foundation)
- Python base64 module (Python Software Foundation)
- RFC 4648: The Base16, Base32, and Base64 Data Encodings (IETF)
- About the Rosetta translation environment (Apple Inc.)
Report a problem with this lesson
Kept only in this browser. Your Learn progress