Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Seven rows hold the differences you meet first · Code size follows the encodings, not the CISC or RISC label · Practise. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

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:

Interactive · step by step Five everyday operations on three ISAs Step through five operations; each box shows how one ISA encodes or handles it.
Five everyday operations on three ISAs, step 5 of 5. Showing x86-64 (syscall, rax = 1), AArch64 (svc #0, x8 = 64), RISC-V with C (ecall, a7 = 64).x86-64syscall, rax = 1AArch64svc #0, x8 = 64RISC-V with Cecall, a7 = 64

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Question 1 of 2 Which ISA branches on a < b without any instruction setting flags first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: RISC-V: blt compares the two registers inside the branch

    x86-64 and AArch64 keep flags and branch on them; RISC-V has none, so its branches compare two registers.

  2. Question 2 of 2 Machine code divides 7 by a divisor that is 0. What happens on each ISA?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: x86-64 raises a divide error, AArch64 writes 0, RISC-V sets every bit of the quotient

    Division by zero is undefined in C, but each ISA defines what its instruction does, so a port can change a crash into a silent wrong value.

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 functions with different needs C · five.c
/* 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:

Bytes and instructions of five functions on four targets Python · size_session.py
"""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:

Interactive · step by step Where the bytes of five functions go Step through the five functions of five.c at -Os; each box shows bytes and instructions for one target.
Where the bytes of five functions go, step 7 of 7. Showing x86-64 (271 B, 77 instructions), AArch64 (168 B, 42 instructions), RV64GC (124 B, 47 instructions), RV64G (188 B, 47 instructions). In focus: x86-64.x86-64271 B, 77 instructionsAArch64168 B, 42 instructionsRV64GC124 B, 47 instructionsRV64G188 B, 47 instructions

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 RV64G needs 188 bytes and RV64GC 122 bytes for the same 47 instructions. By how many percent is RV64G larger, to the nearest whole number?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 54 percent

    188 / 122 is about 1.54, so RV64G is 54 % larger. RV64GC uses 2-byte forms for 33 of the 47 instructions.

  2. Question 2 of 2 x86-64 needs 127 bytes for five.c at -Os and 271 at -O2. What explains most of the growth?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Speed choices the compiler makes for x86-64: unrolling fnv1a, a 32-bit division fast path and loop padding

    The optimisation level changes code size more than the ISA does, so compare targets at the same level and say which one.

Practise

  1. Cover the first section’s table, write its seven rows from memory, then compare.
  2. Add a function to five.c that adds two 64-bit values in memory and stores the sum, predict the smallest target at -Os, then run size_session.py.
  3. Replace -O2 with -Oz, clang’s smallest-code level, in size_session.py and 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.

  1. Question 1 of 12 How long is every AArch64 instruction?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: 4 bytes

    Every A64 instruction is 4 bytes, which is why AArch64 needs more bytes than RV64GC for the same functions.

  2. Question 2 of 12 What does the RISC-V register x0 hold?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: Always 0: reads give 0 and writes are discarded

    A zero register lets one instruction serve as several, such as a move from x0 to clear a register.

  3. Question 3 of 12 Which ISA lets ordinary arithmetic, such as adding 1, work directly on a value in memory?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: x86-64

    Memory operands are why x86-64 needed 11 bytes for bump, against 32 on AArch64 and 18 on RV64GC.

  4. Question 4 of 12 write is system call 1 on x86-64 Linux. What number is it on AArch64 and RISC-V Linux?

    Type a number.

    Show the answer: Question 4 of 12

    Answer: 64

    AArch64 and RISC-V share Linux's generic system-call table, where write is 64; x86-64 has its own table.

  5. Question 5 of 12 Which ISA keeps total store order as its memory model?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: x86-64

    Code that only ever ran on x86-64 can depend on store order without knowing it.

  6. Question 6 of 12 x86-64 code does cmp rdi, rsi and then jl to label. What does the same test become on RISC-V?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: One blt a0, a1, label

    Without flags, RISC-V folds the comparison into the branch; slt is for when the result is needed as a value.

  7. Question 7 of 12 At -Os, which target needed the fewest bytes for the five functions of five.c?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: RV64GC, with 122 bytes

    The ranking at -Os was RV64GC 122, x86-64 127, AArch64 176 and RV64G 188.

  8. Question 8 of 12 How many bytes is the x86-64 movabs that loads a 64-bit constant into a register?

    Type a number.

    Show the answer: Question 8 of 12

    Answer: 10 bytes

    A REX prefix, the opcode and 8 bytes of constant: 10 bytes, the longest instructions in the -Os build.

  9. Question 9 of 12 bump is 11 bytes on x86-64 and 32 bytes on AArch64. Why?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: x86-64 increments each counter with one instruction that has a memory operand; AArch64 needs a load, an add and a store

    Memory operands are the one place where a CISC instruction really does more and costs fewer bytes.

  10. Question 10 of 12 At -Os AArch64 needed 44 instructions and 176 bytes, RV64GC 47 instructions and 122 bytes. How can more instructions take fewer bytes?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: 33 of RV64GC's 47 instructions are 2 bytes long, while every AArch64 instruction is 4

    Instruction count and byte count answer different questions, so a comparison gives both.

  11. Question 11 of 12 With -fcf-protection=full the x86-64 total grows from 127 to 147 bytes. How many bytes does each of the five functions gain?

    Type a number.

    Show the answer: Question 11 of 12

    Answer: 4 bytes

    Each function starts with one endbr64 of 4 bytes, so five functions gain 20 bytes.

  12. Question 12 of 12 Which comparison of code size between two ISAs is fair?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: One source, one compiler and one set of options, with bytes counted per function

    Hold everything but the target fixed, then report bytes and instructions apart.

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.

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.

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.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.