Back to Blog

ISA vs. Microarchitecture, RISC vs. CISC, and the Role of the Compiler

August 18, 202625 min read
Computer Architecture ISA Compilers Learning

Lesson 1, Part 9. We spent Part 8 going down through the memory hierarchy — registers, caches, DRAM, the latency cliffs between them. Let's return to the software side now, and answer a question that's been implicit since the very first line of C++ we wrote in this series: when you type int c = a + b;, what exactly determines the bits the CPU receives, and how much of that is actually about hardware versus convention?

1. The ISA Is a Contract, Not an Implementation

Start with the smallest possible example.

int c = a + b;

A compiler targeting ARM64 might emit:

add w0, w1, w2

That's it. One instruction. But notice what's actually being promised here. The instruction set architecture (ISA) — ARM64 in this case — is making a guarantee that has nothing to do with transistors, pipeline depth, or clock speed. It's making a guarantee about behavior:

"If you feed me this exact bit pattern, I will architecturally produce this exact result — every time, on every chip that implements this ISA, from a $2 microcontroller core to a $10,000 server part."

That's the whole idea. The ISA specifies things like:

Register set (names, widths, how many)
Instruction formats (how bits map to operations)
Memory model (ordering guarantees, atomicity)
Data types (integers, floats, vectors)
Addressing modes (how you compute an effective address)
Exception and interrupt behavior
Privilege levels (user mode vs kernel mode)
Atomic operations (compare-and-swap, load-linked/store-conditional)
SIMD/vector instruction extensions

None of that says how fast it has to happen, or how many circuits it takes. It says only what must be true after the instruction retires. This is precisely the same abstraction-boundary instinct you've already seen twice in this series — combinational vs. sequential logic separating "what a circuit computes" from "how it remembers," and the cache hierarchy separating "what memory means" from "how fast you can get at it." The ISA is that same move applied one layer higher: it separates "what the program means" from "how the silicon realizes it."

This is also, not coincidentally, the exact same shape of contract you already work with every day if you've written against a hardware abstraction layer or an NPU's instruction encoding — the ISA is the API. Everything below it is an implementation detail the caller isn't supposed to have to know about.

Why the contract framing matters

If you've ever wondered why Intel can sell a laptop chip and a server chip that both run the identical compiled .exe, or why an ARM binary compiled in 2015 still runs correctly on a 2026 phone SoC, this is the reason. The ISA is the thing that stays fixed. Everything underneath it — the part we cover next — is free to change release after release, sometimes radically, without breaking a single existing binary.

The ISA is what software is allowed to assume. Microarchitecture is everything a vendor is allowed to change without asking permission.

2. Microarchitecture Is the Implementation Underneath the Contract

Take that same instruction:

add x0, x1, x2

The ISA specification says this must compute x1 + x2 and place the result in x0, honoring the defined register width, flag behavior (if any), and exception semantics. That's all it says. It does not say:

There is exactly one ALU.
The pipeline has five stages.
There is one register file.
Execution happens in program order.

A real implementation is free to build this instruction's execution however it wants, as long as the architecturally visible result is indistinguishable from the contract. Concretely, three very different chips could all correctly implement add x0, x1, x2:

  • A cheap embedded core with one ALU, executing instructions strictly in order, one at a time.
  • A high-end mobile core with eight-wide superscalar issue, out-of-order execution, register renaming, and multiple ALUs competing for the same architectural add.
  • A server chip that internally translates the add into an even lower-level micro-operation, schedules it on whichever of several execution ports is free that cycle, and retires it out of order behind a reorder buffer.

Software cannot tell the difference by inspecting results — only by measuring time. This is the single most load-bearing idea in the entire discipline of computer architecture, so it's worth stating as plainly as possible:

ISA ≠ microarchitecture. The ISA is the specification. The microarchitecture is one specific, replaceable engineering solution to satisfying that specification.

This is exactly why Apple could ship four or five completely redesigned CPU cores (different pipeline depths, different reorder buffer sizes, different branch predictors, different cache hierarchies) across a decade of iPhones while keeping ARM64 as the stable ISA the whole time — and every app in the App Store kept running without recompilation. The ISA held still. The microarchitecture underneath it was rebuilt from scratch, repeatedly, in secret, without anyone asking your app's permission.

It's also why, from a compiler-engineering standpoint, you target the ISA and reason about the microarchitecture. A backend emits ARM64 or x86-64 instructions (the contract); it uses a scheduling model, latency tables, and a target-CPU flag (like -mcpu=apple-m2 or -march=znver4) to guess how a particular microarchitecture will execute those instructions well. Correctness depends only on the ISA. Performance depends on getting the microarchitectural guess right.

3. RISC vs. CISC: Where the Split Came From

Historically, ISAs are grouped into two philosophies.

RISC — Reduced Instruction Set Computer

The RISC idea, which grew out of research at Berkeley and Stanford in the early 1980s (with IBM's earlier 801 project as an intellectual predecessor), pushed instruction sets toward:

  • A small number of simple instructions, each doing roughly one thing.
  • Regular, fixed-width instruction encoding.
  • A strict load/store architecture — arithmetic only ever operates on registers, and only dedicated load/store instructions touch memory.
  • Instructions that are trivial to decode, so the hardware pipeline stays simple and fast.

Examples: ARM, RISC-V, MIPS, and the older SPARC and POWER lines.

CISC — Complex Instruction Set Computer

The CISC philosophy, dominant in the 1970s design era of the VAX and early x86, went the other way:

  • A large number of richer instructions, including ones that combine a memory load, an arithmetic operation, and a memory store into a single instruction.
  • Elaborate addressing modes (base + index + scale + displacement, in one operand).
  • Variable-length instruction encoding, so common operations can be encoded compactly.

The most famous, by a wide margin: x86.

The historical logic behind CISC actually made a lot of sense given its constraints. In the era it was designed, memory was small and slow, and compilers were relatively primitive. Packing more semantic work into each instruction meant smaller binaries and less traffic between CPU and memory — real wins when a machine had kilobytes of RAM and compilers weren't yet trusted to generate optimal code. MIT's 6.823 (Computer System Architecture) frames this directly: CISC machines were designed to simplify compilers and to improve performance under constraints like small and slow memories, and the tendency for a given ISA to imply a particular microarchitectural style (CISC → microcoded, RISC → hardwired and pipelined) followed from that design pressure.

By the early 1980s, those constraints had flipped. Memory got bigger and cheaper, compilers got dramatically better at generating efficient code from simple primitives, and pipelining — which wants every instruction to be roughly the same shape and cost — became the biggest lever for performance. RISC was, in a real sense, a bet that compiler quality would keep improving faster than hand-written assembly could, so it made sense to hand the compiler a small, orthogonal, fast-to-decode instruction set and let it do the work of composing complex behavior out of simple pieces.

The one-paragraph summary of both philosophies

RISCCISC
Instruction countSmall, orthogonal setLarge, many specialized instructions
Instruction lengthFixed (e.g. 32-bit)Variable (e.g. 1–15 bytes on x86-64)
Addressing modesFew, simpleMany, complex
Memory accessOnly via explicit load/storeArithmetic instructions can touch memory directly
Decode complexityLow, highly regularHigh, context-dependent
Typical cycles/instructionClose to 1Historically >1, varies per instruction
Design-era goalCompiler-friendly, pipeline-friendlySmall code size, powerful primitives
ExamplesARM, RISC-V, MIPS, POWERx86, VAX (historical)

4. Why the RISC/CISC Line Blurs Completely in a Modern CPU

Here's the part that trips people up, and it's the part your compiler-and-NPU background will find genuinely interesting rather than just trivia: a modern x86 chip is not, internally, a CISC machine.

When people say "x86 is CISC, so it executes complex instructions directly," that statement was arguably true for a 1978 8086 and is almost entirely false for a 2026 Intel or AMD core. Since the mid-1990s (starting notably with the Pentium Pro / NexGen Nx586 generation), x86 implementations have worked like this:

x86 instruction stream (the architectural ISA — the contract)


   Instruction fetch (variable-length, ugly)


   Decoders (x86 instruction → one or more µops)


   µop stream (fixed-format, RISC-like, simple)


   Out-of-order engine: rename, schedule, execute, retire


   Architectural result — must match the x86 contract exactly

Every complex x86 instruction gets cracked, at decode time, into one or more simpler internal micro-operations (µops) that look a lot like RISC instructions: fixed format, single well-defined operation, easy for the scheduler and register-renamer to reason about. A simple add eax, ebx might decode to a single µop. Something gnarlier, like an instruction that reads a value from memory, adds an immediate, and writes it back, might crack into two or three µops — effectively a load µop, an add µop, and a store µop, executed and scheduled independently once they're inside the out-of-order core.

The internal engine — the reorder buffer, the reservation stations, the execution ports — never sees "x86." It sees a RISC-like µop stream that has already had all of x86's decoding complexity resolved on its behalf. Modern x86 and AMD64 chips have, in effect, built a small RISC computer and stapled an x86-to-µop translator onto the front of it.

The subtlety: it was never really about "complex instructions"

This is the point worth being precise about, because the common shorthand ("CISC instructions are more powerful, so they need more hardware to run") is subtly wrong. The actual hard problem CISC creates is variable-length instruction fetch and decode, not "complexity" of the operation being performed in some abstract sense.

Think about what the front end of a CPU has to do every single cycle: figure out where instruction boundaries are in a raw byte stream, so it knows what to hand the decoders. On a fixed-width ISA like ARM64 or RISC-V, this is nearly free — every instruction is exactly 4 bytes, so instruction N+1 starts at a byte offset you can compute without looking at instruction N at all. You can fetch and predecode many instructions in parallel, because the boundaries are known in advance from the program counter alone.

On x86-64, an instruction can be anywhere from 1 to 15 bytes long, and you cannot know how long an instruction is until you've partially decoded it — prefixes, opcode bytes, a ModRM byte, possibly a SIB byte, possibly a displacement, possibly an immediate, each of which can be present or absent depending on earlier bytes. Finding where the next instruction starts requires decoding the current one first. That's an inherently serial dependency sitting directly in the hottest, most frequently executed part of the whole pipeline — the fetch/decode front end that runs on every single instruction, every single cycle, for the life of the program.

This is why x86 decoders are one of the most heavily engineered, most heavily patented parts of an Intel or AMD chip: multiple parallel decoders (short ones for common short instructions, at least one "complex" decoder for the gnarly cases), branch predictors that also have to predict instruction lengths to keep the front end fed, and — critically — a µop cache that stores already-decoded µops so that hot loops can skip the expensive variable-length decode step entirely on repeated iterations. None of that machinery exists on a fixed-width ISA, because there's nothing hard to solve: decode bandwidth scales almost linearly with however many decoder units you're willing to place in silicon, since each one can independently find its own instruction boundary.

So the honest framing is: RISC vs. CISC was never really a fight about how powerful an individual instruction is allowed to be. It was a fight about whether instruction boundaries are cheap to find. Fixed-width encoding makes that answer trivially "yes." Variable-length encoding makes it expensive enough that entire generations of microarchitects have built their careers solving it — and once solved (by translating into a fixed-format µop stream as early as possible), the rest of the out-of-order machine gets to be RISC-shaped regardless of which ISA is feeding it.

Instruction format, side by side

PropertyARM64 / RISC-V (fixed-width)x86-64 (variable-length)
Instruction sizeAlways 32 bits1 to 15 bytes
Finding next instructionPC + 4, always, no decode neededMust partially decode current instruction first
Parallel fetch of N instructionsTrivial — N independent 4-byte readsHard — each boundary depends on the previous instruction
Decoder hardwareMultiple identical, simple decodersAsymmetric decoders (simple + complex) plus a µop cache
Code densityLower (every instruction pays the full 4 bytes)Higher (short, common instructions can be 1–2 bytes)
Internal execution shape (modern chips)Already RISC-likeDecoded into a RISC-like µop stream before execution

Code density is the one real advantage CISC keeps in this comparison, and it's not nothing — it's part of why ARM later added Thumb (a compressed 16-bit encoding) and why RISC-V has a "C" compressed-instruction extension: both are explicit admissions that pure fixed-width 32-bit encoding costs you instruction-cache footprint, and it's worth clawing some of that back without reintroducing x86-style variable-length decode pain.

5. Machine Code vs. Assembly — Making the Layers Precise

It's worth being pedantic about terminology here, because these words get used loosely.

  • Machine code is the actual sequence of bits the CPU's front end reads and decodes. Nothing about it is human-readable by design.
  • Assembly language is a human-readable, roughly one-to-one textual notation for machine code — a mnemonic and operand syntax that an assembler translates into the corresponding bits.

So add x0, x1, x2 is assembly. What the CPU actually receives is something conceptually like:

10001011000000100000000000000000

(illustrative bit pattern only — the exact encoding depends on the ISA's instruction format, and real ARM64 add-immediate/add-register encodings have specific field layouts for opcode, registers, and shift; the point is only that it's a fixed pattern of bits, not the mnemonic text)

The relationship is a strict pipeline:

Assembly source
      │  (assembler)

Machine code (object file, eventually linked into an executable)

And one level up:

C / C++ source
      │  (compiler)

Assembly / machine code

Two separate translation steps, two separate tools, each with a well-defined job. This distinction matters in practice: when you read a disassembly listing (say, from objdump or a debugger), you are looking at assembly that some tool reconstructed from machine code after the fact — it's a readable projection of the bits, not the thing the CPU actually executes.

6. A Worked Example: One C++ Line, Two Different ISAs

This is where it's worth being concrete instead of abstract, because "the ISA is a different contract" is easy to nod along to and easy to misunderstand in practice. Take the simplest possible operation:

int add(int a, int b) {
    return a + b;
}

By the standard calling conventions, a and b arrive in registers, and the return value goes out in a register. On ARM64 (following the AAPCS64 calling convention, where integer arguments arrive in w0w7 and the return value goes out in w0), a plausible compilation looks like:

; ARM64 — illustrative, simplified
add:
    add w0, w0, w1     ; w0 = a + b, result already in return register
    ret

Three things worth noticing. First, a arrives in w0 and the result also needs to leave in w0, so the compiler can compute directly into the destination register — no extra move required. Second, this is one instruction, full stop, because ARM64's add is a pure register-to-register operation with no memory access built in — this is the load/store philosophy showing up directly in the generated code. Third, the encoding of that one instruction is a fixed 32 bits, and the assembler doesn't need to think about how long it is.

On x86-64 (System V AMD64 calling convention, where the first two integer arguments arrive in edi and esi, and the return value goes out in eax), a plausible compilation looks like:

; x86-64 — illustrative, simplified
add:
    mov  eax, edi      ; eax = a
    add  eax, esi      ; eax = eax + b  →  eax = a + b
    ret

Here the calling convention puts the arguments in different registers than the return value lives in, so the compiler typically has to insert a mov first — an extra instruction that exists purely because of where the ABI decided to put things, not because of any inherent property of addition. add eax, esi is itself a variable-length x86 instruction (in this case a short one, but the point stands: its length is a function of its operand encoding, not a fixed slot size the way add w0, w0, w1 is).

Both snippets compute the identical mathematical result. Both correctly implement int add(int a, int b) { return a + b; } as specified by the C++ standard. Neither is "more correct" than the other. What differs is entirely a property of the ISA contract each one is targeting: which registers the calling convention assigns, whether the ISA permits computing directly into the destination, and how many bytes the resulting instructions occupy in memory. This is the ISA-as-contract idea from Section 1, made concrete at the level of two or three real instructions instead of a slogan.

(A note on rigor: the instruction choices above reflect how these compilers behave in typical, unoptimized-to-lightly-optimized code generation for this exact pattern — they are illustrative and simplified to make the point about calling conventions and encoding, not a verbatim dump from a specific compiler version and optimization level. If you want the literal, current output of GCC, Clang, or MSVC for a given -O level and target CPU, Compiler Explorer — godbolt.org — is the right tool; it will show you the real thing, including cases where a smart compiler optimizes this exact function down to something even simpler.)

7. The Compiler's Job: Walking C++ Down to Bits

Now we can put a name to every stage of the transformation, because you already know most of these stages from the tools you use professionally.

int foo(int a, int b) {
    return a + b;
}

The path from this source to executed bits looks like:

C++ source
    │  parsing

AST (Abstract Syntax Tree)
    │  semantic analysis, lowering

IR (Intermediate Representation)
    │  optimization passes

IR (optimized)
    │  instruction selection

Target-specific instructions (still somewhat abstract)
    │  register allocation

Target-specific instructions (concrete registers assigned)
    │  assembly emission

Assembly text
    │  assembler

Machine code (object file)
    │  linker

Executable

Each stage exists to solve one specific, separable problem. It's worth walking through why, not just what, because "why does this stage exist" is the question that actually transfers to new compilers you'll encounter:

Parsing → AST. The compiler needs a structured representation of what you wrote before it can reason about it at all. The AST captures syntax and rough semantics (this is a function, this is a binary expression, these are its operands) but it's still shaped like your source code — full of source-language-specific concepts like "this is a C++ for loop" or "this is a template instantiation."

AST → IR. This is the step that matters most for everything downstream, and it's the one your MLIR and LLVM background will recognize immediately. The compiler lowers the AST into an intermediate representation that has deliberately shed source-language concepts (no more "this was a for loop," just basic blocks and explicit control flow edges) but hasn't yet committed to any target machine's specifics (no registers yet, typically an unbounded set of virtual values in SSA form — every value assigned exactly once, which is what makes later optimization passes tractable). This is exactly the layer LLVM IR operates at, and it's exactly the layer MLIR generalizes into a whole family of IRs (dialects) for the same underlying reason: you want one representation that many different frontends can lower into, and many different backends can lower out of, so that an optimization written once (say, dead-code elimination, or common-subexpression elimination) works for every frontend/backend pair without being rewritten per target. This is the entire economic argument for having an IR at all — without it, you'd need a separate optimizer for every (source language × target ISA) combination instead of one optimizer that sits in the middle and serves all of them. If you've written an MLIR pass or touched an NPU compiler's lowering pipeline, this is precisely the design pattern you've already used: a machine-independent middle representation that decouples "what does this program mean" from "what hardware will run it."

Optimization (IR → IR). With the program in a machine-independent, analyzable form, the compiler runs passes over it: constant folding, dead-code elimination, inlining, loop-invariant code motion, common-subexpression elimination, and eventually target-aware passes like auto-vectorization. Crucially, most of these passes don't need to know or care whether the final target is ARM64, x86-64, or an NPU — they're reasoning about the IR's semantics, not the hardware's.

Instruction selection (IR → target-specific instructions). Here the compiler finally commits to a real ISA. It pattern-matches chunks of IR onto sequences of real target instructions — this is the stage that decides "this IR add maps to an ARM64 add instruction" or, in a fused-multiply-add-friendly target, "this IR multiply-then-add maps to a single fma instruction." This is also the stage where ISA-specific tricks live: knowing that x86 has an addressing mode that can compute base + index*scale + displacement for free as part of a load, for instance, and choosing to fold an array-indexing computation into that addressing mode instead of emitting a separate add.

Register allocation. IR (and even early target-specific instruction selection) typically assumes an unlimited number of virtual registers/values. Real hardware has a small, fixed number of physical registers — sixteen or thirty-two general-purpose ones, not infinite. Register allocation maps the unbounded virtual set down onto the small physical set, deciding which values live in registers at any given point and which get "spilled" to the stack when there simply aren't enough registers to go around. This is a genuinely hard combinatorial problem (graph coloring is the classic formulation), and it's one of the places where compile-time cost and output quality trade off most visibly.

Assembly emission → assembler → machine code. The final textual assembly gets turned into the literal bit patterns the ISA's instruction format specifies, and packaged into an object file with the metadata (symbol tables, relocation info) the linker needs to stitch multiple compiled units into one executable.

The reason to walk through all of this explicitly, rather than just saying "the compiler compiles it," is that each stage is answering a different question, and the abstraction boundary between "IR" and "target-specific instructions" is the exact same kind of boundary as ISA vs. microarchitecture, one layer up the stack: IR is the machine-independent contract the optimizer relies on; instruction selection is where that contract finally gets committed to one specific, replaceable target.

8. Why Identical C++ Can Produce Wildly Different Performance

Put the last two sections together and a fact that seemed almost paradoxical earlier in this series stops being mysterious: two pieces of C++ that compute the same mathematical result can execute at wildly different speeds, because "the same result" only constrains the IR's semantics — it says nothing about which instructions get selected, and instruction selection is where the real performance decisions get made.

Take:

float sum = 0.0f;
for (int i = 0; i < N; i++)
    sum += A[i];

A conservative, non-vectorizing compilation processes this one element at a time:

load A[i]
add sum, sum, A[i]
increment i
compare, branch

repeated N times — one scalar add per element.

A vectorizing compiler, recognizing the loop has no cross-iteration data hazard it can't work around, can instead emit SIMD instructions that operate on several elements per instruction:

load A[i..i+3]      (4 floats packed into one 128-bit register)
vadd  sum_vec, sum_vec, A_vec     ; 4 adds in one instruction
increment i by 4
compare, branch
horizontal-reduce sum_vec → sum   ; combine the 4 partial sums at the end

If a 128-bit vector register holds four 32-bit floats, then:

[A0 A1 A2 A3]
        +
[B0 B1 B2 B3]
        =
[C0 C1 C2 C3]

happens in a single vector instruction instead of four scalar ones. That's data-level parallelism, and it's a direct consequence of the ISA exposing SIMD instructions (NEON on ARM, AVX/AVX2/AVX-512 on x86, SVE on newer ARM server parts) as part of its contract, and the compiler's instruction-selection stage choosing to target them.

This is also exactly why the -mcpu / -march / -mtune flags you already reach for professionally matter so much: they tell instruction selection which parts of the ISA's contract it's allowed to assume are present (does this target actually have AVX-512, or only AVX2?) and which microarchitectural scheduling model to optimize against (this is a Zen 4 core, favor its execution port layout). Get the target wrong and the compiler either falls back to conservative scalar code it knows is universally safe, or — worse — emits instructions the actual chip doesn't support at all.

We'll come back to this with real depth later in the series: SIMD instruction sets, vector processors, GPUs, and tensor accelerators are all, at their core, the same idea taken further and further — do more architecturally-defined work per instruction, and let the ISA expose that as a contract the compiler can target.

Further Reading

  • Patterson, D. A. & Hennessy, J. L., Computer Organization and Design: The Hardware/Software Interface — the canonical treatment of ISA design, RISC vs. CISC, and instruction formats, from one of the architects of the original RISC and RISC-V projects.
  • Cooper, K. D. & Torczon, L., Engineering a Compiler — the standard reference on compiler pipeline stages, IR design, instruction selection, and register allocation.
  • LLVM Language Reference Manual — the official specification of LLVM IR as a machine-independent, SSA-based representation shared across every phase of the LLVM compilation strategy.
  • MIT OCW 6.823, Computer System Architecture — lecture materials covering ISA design tradeoffs, including how CISC and RISC philosophies grew out of different assumptions about memory size and compiler quality.
  • MIT OCW 6.004, Computation Structures — foundational course building a RISC processor from the gate level up.
  • RISC vs CISC — GeeksforGeeks — a concise comparison of instruction formats, addressing modes, and decode complexity between the two philosophies.
  • "The legend of 'x86 CPUs decode instructions into RISC form internally'" — discussion thread with useful nuance on what "RISC-like µops" does and doesn't mean in modern x86 implementations.

Next, in the closing chapter of Lesson 1: Parallelism, the Modern CPU, and the Big-Picture Mental Model — pulling every piece from this lesson (logic, pipelining, memory hierarchy, ISA, compilers) into one coherent picture of what actually happens, top to bottom, when a modern CPU runs your program.