Codegentarget

What a Backend Must Know About Its Target

Four things, and none of them optional: the instruction set, the register file, the calling convention and the memory model. x86-64, ARM64, RISC-V and WebAssembly answer all four differently — and one of them has no registers at all.

The question

What does a compiler backend actually need to know about the machine it is generating code for?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

A target description: an instruction set with operand shapes and costs, a register file with classes and aliasing, a calling convention, a memory model, and a data layout — sizes, alignments and endianness. In LLVM this is literally a set of TableGen files plus a DataLayout string. It exists so that the same machine IR can be parameterised by target rather than duplicated per target, which is what makes one backend framework serve many machines.

What this phase may assume or do

The backend may emit only instructions the target actually implements at the specified subtarget level, may use only registers the calling convention leaves available at each point, and may reorder memory operations only as far as the target's memory model permits — which is a *different* limit from what the source language permits, and the stricter of the two governs. Assuming an ISA extension that the deployment CPU lacks is not a performance mistake but a correctness one: the process dies with an illegal instruction.

Key points

  • A target is four things: instruction set, register file, calling convention, memory model — plus a data layout of sizes, alignments and endianness.
  • The same ISA with a different ABI is a different target, which is why Linux and Windows x86-64 need different backend configuration.
  • Register counts differ enough to change allocation strategy: 16 on x86-64, 31 on AArch64, 31 usable on RISC-V, and none at all on WebAssembly.
  • x86-64 has a strong memory model and a shared flags register; AArch64 and RISC-V are weakly ordered and write flags only when asked.
  • Targeting WebAssembly removes register allocation and scheduling and adds the problem of reconstructing structured control flow from an arbitrary CFG.
  • -march is a correctness setting and -mtune is a performance setting; confusing them ships binaries that crash on part of the fleet.

Four questions, per target

A backend is parameterised by four things, and getting any of them wrong produces a different class of bug. The instruction set says what operations exist and in what operand shapes — whether arithmetic is two- or three-operand, whether there is a fused multiply-add, whether unaligned loads work. The register file says how many registers there are, what classes they fall into, and how they alias — on x86-64 eax is the low half of rax and writing it zeroes the top, which is a fact the allocator must model.

The calling convention says where arguments and returns live and which registers survive a call. And the memory model says which reorderings of memory operations the hardware may perform, which bounds what the compiler may do on top — see [[memory-model]] in the concurrency domain for the hardware and language halves.

Notice that only the first is what people usually mean by "the architecture". The other three are why the same ISA with a different ABI is effectively a different target, and why x86_64-pc-windows-msvc and x86_64-unknown-linux-gnu need different backend configuration despite being the same instruction set — the subject of [[target-triples]].

Four targets, four sets of answerstarget
x86-64AArch64RISC-V (RV64GC)WebAssembly
Integer registerstarget16, several with legacy roles31 general-purpose plus a zero register32, of which x0 is hardwired zeroNone — a stack machine with typed locals
Arithmetic formtargetTwo-operand, destructiveThree-operandThree-operandStack-based: pop two, push one
Instruction lengthtarget1–15 bytes4 bytes fixed4 bytes, or 2 with the C extensionVariable, LEB128-encoded
Condition flagstargetA shared flags register written by most arithmeticWritten only by explicit s-suffixed formsNone — compare-and-branch instructions insteadNone
Memory modeltargetStrong (TSO): only store-load may be reorderedWeak: needs explicit barriers or acquire/release formsWeak, with explicit fence instructionsSequentially consistent for its own atomics
Argument registerstargetrdi rsi rdx rcx r8 r9 on System V; rcx rdx r8 r9 on Windowsx0x7a0a7Values on the operand stack; locals are indexed
What the backend worries about mosttargetTwo-operand copies and the flags registerConstant materialisation and branch rangeInstruction count, since addressing modes are minimalNo registers to allocate; structured control flow to reconstruct

WebAssembly is the interesting one

specThat WebAssembly has structured control flow, a stack machine and linear memory is fixed by the WebAssembly specification, not by any engine. What varies between engines is what they compile it *to* and how fast — V8, SpiderMonkey and Wasmtime all make different tiering decisions for the same module.

Three of the four targets above are machines. WebAssembly is not: it is a portable compilation target with a stack machine, structured control flow, no registers and no addresses in the usual sense. Backing it changes what a backend even means.

Register allocation, the subject of an entire module of this domain, does not exist — values live on an operand stack or in indexed locals, and the consumer of the wasm will do its own allocation when it compiles to the real machine. Instruction scheduling does not exist for the same reason. What replaces them is a problem no native backend has: wasm control flow is *structured*, made of blocks, loops and ifs rather than arbitrary jumps, so an arbitrary CFG must be converted back into nested structure — the relooper problem — and irreducible control flow may need node duplication to express at all.

This is the general shape of targeting a virtual ISA. You give up the machine-specific decisions and inherit a different set of constraints from the abstraction, and you defer the real backend work to whoever consumes your output — see [[wasm-vs-native]].

  • No register allocation: values are on a stack or in locals, and the engine allocates real registers later.
  • No instruction scheduling: the engine schedules when it compiles the module.
  • Structured control flow only: an arbitrary CFG must be re-expressed as nested blocks and loops.
  • Linear memory with bounds enforced by the engine, so a pointer is an index rather than an address — which is why wasm is sandboxed by construction.
  • The compile that matters happens twice: yours to wasm, and the engine's to machine code, on the user's device.

Subtargets: the same architecture is many targets

targetThe -march / -mtune split is the GCC and Clang spelling on x86. On ARM targets the equivalents are -mcpu and -mtune, and on MSVC the switch is /arch:. Rust spells it -C target-cpu and -C target-feature. The distinction between "may emit" and "optimise for" exists in all of them, with different names and slightly different defaults.

Naming an architecture is not enough. "x86-64" spans two decades of CPUs with wildly different feature sets — a binary using AVX-512 runs on some server parts and faults on most desktops; one using SSE2 runs everywhere. So a target is really an architecture plus a *subtarget*: a feature set and a tuning model.

Those two are separable and it is worth being precise about the difference. -march= (or -mcpu= on ARM) says which instructions the compiler may emit, and getting it wrong produces an illegal-instruction crash on the machines that lack them. -mtune= says which microarchitecture to optimise the scheduling and cost models for, and getting it wrong produces code that is merely somewhat slower. The first is a correctness setting; the second is a performance setting, and conflating them is how "it works on my machine" becomes a production incident.

The -march=native habit deserves particular suspicion in any build that ships anywhere but the build machine. It means "use everything this CPU has", which is exactly the wrong answer for an artefact that will run on a fleet. The usual production answer is a conservative baseline plus run-time dispatch: compile several versions of the hot function and select at startup based on CPU feature detection, which is what mainstream numerical libraries do.

How it works

The steps, in the order the compiler takes them.

  • The target description declares the instruction set with operand constraints, latencies and encodings, usually declaratively so that selector, assembler and scheduler are generated from one source.
  • It declares register classes and the aliasing between registers, so the allocator knows that writing one register affects another.
  • It declares the calling convention as a set of rules assigning arguments to registers and stack slots by type and position, and names the callee-saved set.
  • It declares a data layout — pointer size, integer and floating-point alignments, endianness — which the middle-end consults for struct layout and legality of type-punning.
  • A subtarget selects a feature set (which instructions may be emitted) and a tuning model (which costs and latencies to optimise against), independently.
  • The backend consults all of it during selection, allocation and scheduling; nothing above the backend is supposed to know any of it, though data layout leaks upward by necessity.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A binary built with -march=native on a build machine faults with SIGILL on older fleet machines, and the crash reports show an instruction nobody in the team recognises.
  • Code that is correct on x86-64 races on AArch64, because the developer's mental model of memory ordering came from a strongly-ordered machine and the weakly-ordered one reorders what the code assumed it could not.
  • A struct is passed by value between two components compiled with different assumptions about the ABI, and fields arrive shifted. There is no link error, only wrong data.
  • A wasm build of a working native program blows the engine's limits or runs several times slower, because the algorithm depended on unaligned access or on control flow that structured form expresses badly.
  • A pointer-size assumption baked into serialisation code (8 bytes) breaks on a 32-bit target such as wasm32, where pointers are 4 bytes and a struct laid out by hand no longer matches.

When it helps

  • Choosing a deployment baseline: knowing that feature selection is a correctness setting and tuning is not settles most of the argument about build flags.
  • Porting: the four questions are a checklist, and the memory-model answer is the one that finds the bugs nobody expected.
  • Reading generated code for an unfamiliar architecture — knowing the register count and whether arithmetic is three-operand explains most of what looks strange.

When it hurts

  • Over-specialising a build to one microarchitecture for a fleet that is not homogeneous. The gain is usually small and the crash surface is not.
  • Assuming knowledge transfers between architectures. x86-64's strong memory model is the most dangerous thing an engineer can carry to AArch64, because incorrect code appears to work for years first.

What it costs

Every one of these is paid by something.

  • A richer target description buys better code and costs a large, per-target artefact that must stay consistent with the encoder, the scheduler and the ABI documents — and that goes stale as new CPUs ship.
  • Targeting a specific subtarget buys the newest instructions and costs portability: every machine in the fleet must have those features, forever, or the binary faults.
  • Targeting a virtual ISA such as WebAssembly buys portability and sandboxing and costs a second compilation on the user's device, plus the loss of every machine-specific optimization you would have made.

What else you could do

What a different compiler or language does instead, and when that is better.

  • Run-time dispatch: compile several variants of the hot code and select on CPU feature detection at startup. Costs binary size and a dispatch layer; buys a conservative baseline with no performance sacrifice where it matters.
  • Function multiversioning, where the compiler generates the variants and the dispatch for you (__attribute__((target_clones)) in GCC and Clang). Less control, much less code.
  • Ship IR and compile on the target: what Android did with the Dalvik-to-ART transition, and what a wasm module does inherently. You gain exact tuning and pay with install-time or start-time compilation.
  • Do not choose: build separate artefacts per architecture and let the package manager pick, which is what every Linux distribution and every multi-arch container image does.

See it for yourself

The flag, dump or tool that shows you this directly.

  • What features are on by default: clang -E - -march=native -### < /dev/null prints the full driver invocation including every implied feature flag.
  • What the compiler knows about a CPU: llc -mattr=help lists every subtarget feature for the current target, and llc -march=aarch64 -mattr=help for another.
  • Cross-compare code generation: clang -O2 -S --target=aarch64-unknown-linux-gnu beside the x86-64 output for the same source.
  • The data layout: clang -S -emit-llvm -o - file.c | head -3 prints the target datalayout and target triple lines that parameterise everything downstream.
  • What is in a binary: objdump -d binary | grep -c vmovdqa64 will tell you quickly whether an AVX-512 instruction made it into an artefact you meant to keep portable.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "The target is the CPU architecture." The target is the architecture *and* the ABI *and* the OS conventions. Linux and Windows on the same x86-64 chip are different targets.
  • "More registers is strictly better." More registers reduces spilling and increases the cost of saving them across calls and context switches. 16 versus 32 changes the allocator's job, not its difficulty.
  • "WebAssembly is a bytecode, so it is like the JVM." It is a stack machine like the JVM and differs in the ways that matter here: structured control flow, linear memory, no garbage collection in the core specification, and a design intended for ahead-of-time compilation to machine code rather than interpretation.
  • "-march=native makes my program faster." It makes the program use whatever the build machine has. Whether that is faster depends on the code, and whether it *runs* depends on the deployment machine having the same features.

Misconceptions

The claim, and what is actually true.

A backend targets an instruction set.
It targets an instruction set plus a register file plus a calling convention plus a memory model plus a data layout. Change only the calling convention and you have a different target that will not interoperate.
RISC means fewer instructions, so RISC-V programs are smaller.
RISC means simple, regular instructions, which usually means *more* of them for the same work and larger code. The compressed extension exists precisely to claw some of that back.
Compiling for WebAssembly is like compiling for another CPU.
It removes register allocation and scheduling entirely and adds a control-flow structuring problem no native target has. The backend is a genuinely different shape, not a retarget.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

Before a backend can emit anything it needs to know four things about the machine: which instructions exist, how many registers there are, where function arguments go, and what the hardware is allowed to reorder in memory. x86-64, ARM64 and RISC-V answer all four differently, and WebAssembly answers "none" to the register question because it is a stack machine rather than a CPU.

practical

The practical stake is build flags. -march/-mcpu decides which instructions may be emitted and is a correctness setting: too aggressive and part of your fleet gets SIGILL. -mtune only changes cost models and is a performance setting. For anything shipped beyond the build machine, pick a conservative baseline and use run-time dispatch for the handful of functions that genuinely benefit from newer instructions.

advanced

The four-part target description is also an argument about where portability should live. A native backend absorbs all four and produces something that runs only on that machine. A virtual ISA absorbs none of them and pushes the whole problem to the consumer, which is why wasm modules run everywhere and why a wasm module is not a program until an engine has compiled it. In between sit the interesting hybrids: ship IR and compile at install time; ship a fat binary with per-architecture slices; ship one binary with run-time dispatch. Each of those is a different answer to "when do we commit to a machine", and the whole spectrum from [[aot-compilation]] to [[jit-compilation]] is really this one question asked at different times.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

targetRegister counts, argument registers, flag behavior and memory-ordering strength in the matrix are properties of the named architectures and ABIs. The x86-64 row assumes System V unless it says Windows; the AArch64 row assumes AAPCS. The memory-model column describes the hardware's permissiveness, which is a bound on — not the same thing as — what a source language's memory model allows.
specThe WebAssembly column is fixed by the WebAssembly core specification: a stack machine, structured control flow and linear memory are normative, not engine choices. What each engine compiles a module *to*, and when, varies widely.
implementationThat -march controls which instructions may be emitted while -mtune controls only cost modelling is the GCC and Clang behavior on x86 targets. Other compilers spell it differently and some conflate the two; MSVC's /arch: and Rust's -C target-cpu have similar but not identical semantics. Check the specific toolchain before relying on the distinction.

If you were asked this in an interview

  • Name what a backend needs to know about a target beyond the instruction set, and give a bug that results from getting each one wrong.
  • What changes about a backend when the target is WebAssembly rather than a CPU?
  • Why is -march a correctness setting and -mtune not?

Connections

OS & Networkingprocess-memory-layout
Domains that do not exist yet
  • DevOps / Production Engineering — Choosing a build baseline for a heterogeneous fleet, and shipping per-architecture artefacts
    The decision about which instruction set extensions a production binary may use is a deployment decision as much as a compiler one, and the machinery for building and distributing per-architecture artefacts is owned there.