Introduction to Rust

Compiled Languages and LLVM

Rust is an ahead-of-time (AOT) compiled language that translates human-readable source code directly into native machine instructions before the program is ever executed. To optimize and emit machine code for dozens of processor architectures, rustc uses LLVM (Low Level Virtual Machine) as its primary compiler backend.

Ahead-of-Time (AOT) Compilation vs Other Execution Models

Programming languages execute code through three main strategies:

Execution Model Examples How It Works Startup Time Peak Throughput Deployment Artifact
Ahead-of-Time (AOT) Compiled Rust, C, C++, Go, Zig Source code is translated directly into native CPU instructions (x86_64, arm64) at build time Instant (milliseconds) Maximum (native CPU execution) Standalone native binary executable
Bytecode + Just-in-Time (JIT) VM Java (JVM), C# (.NET), JavaScript (V8) Source compiles to portable bytecode; a runtime VM interprets and JIT-compiles hot paths at runtime Moderate (VM warmup & JIT compilation) High after warmup, with GC pauses Bytecode archive (.jar, .dll) + target VM installation
Bytecode Interpreter Python (CPython), Ruby Source is parsed into bytecode and stepped through by an interpreter loop at runtime Fast to moderate 10×–100× slower on CPU-bound code Source scripts + interpreter installation
Ahead-of-Time (AOT) Model Used by Rust:
  Build Machine                                   Target Machine
  +---------------+     +---------------+         +-------------------+
  | Source (.rs)  | --> | rustc + LLVM  | ------> | Native Binary     | --> Runs directly on CPU
  +---------------+     +---------------+         | (ELF/Mach-O/.exe) |     (No VM or Rust needed)
                                                  +-------------------+

Because Rust compiles ahead of time into a self-contained binary, the target machine does not need rustc, cargo, or any runtime virtual machine installed to execute the program.

Modular Compiler Architecture: Frontend vs Backend

Modern compilers separate language-specific analysis from hardware-specific code generation:

+-----------------------------------+       +-----------------------------------+
|         Compiler Frontend         |       |         Compiler Backend          |
|              (rustc)              |       |              (LLVM)               |
|-----------------------------------|       |-----------------------------------|
| - Lexing & parsing Rust syntax    |       | - Dead-code elimination           |
| - Macro expansion                 |       | - Loop unrolling & inlining       |
| - Type inference & trait solving  | ----> | - Auto-vectorization (SIMD)       |
| - Borrow checking & lifetimes     | LLVM  | - Register allocation             |
| - Lowering AST -> HIR -> MIR      |  IR   | - Instruction scheduling          |
+-----------------------------------+       | - Emitting x86_64 / ARM / WASM    |
                                            +-----------------------------------+
  1. The Frontend (rustc): Understands Rust syntax, types, traits, ownership, and lifetimes. Once it verifies that the program is memory-safe and type-correct, it translates the program into LLVM Intermediate Representation (LLVM IR).
  2. The Backend (LLVM): A language-independent optimization and code-generation engine (also used by Clang for C/C++, Swift, and Julia). LLVM operates on LLVM IR without needing to know that the original code came from Rust.

What LLVM Does for Rust

Delegating code generation to LLVM gives Rust three major advantages:

1. Industrial-Strength Optimizations

LLVM applies hundreds of optimization passes across your code (-C opt-level=2 or 3 in --release builds):

  • Function Inlining: Replaces function call instructions with the body of the called function, eliminating call overhead and exposing further optimizations across function boundaries.
  • Constant Folding and Propagation: Evaluates arithmetic and branches with known inputs at compile time (1024 * 64 becomes 65536 directly in the instruction stream).
  • Loop Unrolling and Auto-Vectorization: Converts scalar loops over arrays or iterators into single-instruction, multiple-data (SIMD) vector instructions (such as SSE/AVX on x86_64 or NEON on Apple Silicon ARM64).
  • Dead-Code Elimination: Strips unreachable branches and unused private functions from the final binary.
  • Link-Time Optimization (LTO): When enabled (lto = true in Cargo.toml), LLVM optimizes across crate boundaries across the entire dependency graph as a single unit.

2. Multi-Architecture Cross-Compilation

Because LLVM has mature backends for nearly every CPU architecture in existence, rustc can target servers, desktops, smartphones, browsers, and microcontrollers simply by selecting a target triple (<arch>-<vendor>-<os>-<env>):

Target Triple Platform
aarch64-apple-darwin macOS on Apple Silicon (M1–M4)
x86_64-unknown-linux-gnu 64-bit Linux with GNU libc
x86_64-pc-windows-msvc 64-bit Windows with MSVC ABI
wasm32-unknown-unknown WebAssembly (runs in web browsers and Wasm runtimes)
thumbv7em-none-eabihf Bare-metal ARM Cortex-M4F/M7 microcontrollers
riscv64gc-unknown-linux-gnu 64-bit RISC-V Linux

3. Shared C/C++ Tooling and Sanitizers

Because rustc and clang share the same LLVM backend infrastructure, Rust binaries work natively with standard native debugging and profiling tools:

  • Debuggers: lldb and gdb inspect Rust stack frames and variables using standard DWARF/PDB debug info.
  • Profilers: perf (Linux), Instruments (macOS), and Valgrind/Cachegrind profile Rust binaries directly.
  • Cross-language LTO: Rust and C/C++ object files compiled with matching LLVM versions can be inlined into one another at link time without FFI call overhead.

Inspecting LLVM IR and Native Assembly

You can observe how rustc and LLVM optimize a Rust function into LLVM IR and native machine assembly using --emit:

// sum.rs
pub fn sum_to_n(n: u64) -> u64 {
    (1..=n).sum()
}

Compile with optimizations (-O) and emit both LLVM IR (.ll) and assembly (.s):

rustc --edition 2024 -O --crate-type lib --emit llvm-ir,asm sum.rs

Instead of emitting a runtime loop that adds numbers from 1 to n one by one, LLVM recognizes the arithmetic progression and replaces the entire loop with Gauss's closed-form formula $\frac{n(n + 1)}{2}$ in constant time $O(1)$!