## how Rust compiles

#### i promise it's actually doing something useful while you wait

the Rust compilation model has surprising effects

## introduction to myself

Noratrieb (she/her)
contributing to the compiler since 2021
# speed 🚀 - runtime performance - compile times

## what does rustc like, do?

#### a quick overview of the compilation phases

## the frontend and the backend

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    subgraph compiler
                      frontend --> backend
                    end
                    source --> frontend
                    backend --> binary

## it all starts at the source

                  pub fn add(a: u8, b: u8) -> u8 {
                    a.wrapping_add(b)
                  }

## until it doesn't even look like Rust anymore

MIR

## further going to LLVM IR ``` ; meow::add define noundef i8 @add(i8 noundef %a, i8 noundef %b) #0 { start: %_0 = add i8 %b, %a ret i8 %_0 } ```
## and then you're done ``` <add>: lea (%rsi,%rdi,1),%eax ret ```
## ok but why does my program compile so slowly now?
## it's often LLVMs fault - but like not really <br> <img alt="output of cargo build --timings, showing blocks of blue and purple bars of roughly equal size" src="cargo-timings-debug-ra.png"> `cargo build --timings`

## a crate - the compilation unit

quite big

in C it's just a single file

## a codegen unit

LLVM is single-threaded

rustc: hi LLVM, look we are like a C file, now be fast

~1-256 depending on size and configuration

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    crate

                    crate --> cgu1["Codegen-Unit 1"]
                    crate --> cgu2["Codegen-Unit 2"]
                    crate --> cgu3["Codegen-Unit 3"]

## codegen units

    fn main() {}

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    mainmir["main (MIR)"]

                    subgraph mycgu1[my CGU 1]
                      mainll["main (LLVM IR)"]
                    end

                    mycgu1 --> |LLVM| mycgu1.rcgu.o

                    mainmir --> mainll

                    mycgu1.rcgu.o --> |link| my_binary
                    std["std (and others)"] --> |link| my_binary

## the linker can be a slow part for incremental builds - LLD (Linux (default for x86-64), Windows) - [mold (Linux)](https://github.com/rui314/mold) - [wild (Linux, experimental)](https://github.com/davidlattimore/wild) - MacOS default ld64 is fast already

## codegen units (but more)

    fn main() {}
    mod foos {
      fn foo1() {}
      fn foo2() {}
    }

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    mainmir["main (MIR)"]
                    foo1mir["foo1 (MIR)"]
                    foo2mir["foo2 (MIR)"]

                    subgraph mycgu1[my CGU 1]
                      mainll["main (LLVM IR)"]
                    end
                    subgraph mycgu2[my CGU 2]
                      foo1ll["foo1 (LLVM IR)"]
                      foo2ll["foo2 (LLVM IR)"]
                    end

                    mycgu1 --> mycgu1.rcgu.o
                    mycgu2 --> mycgu2.rcgu.o

                    mainmir --> mainll
                    foo1mir --> foo1ll
                    foo2mir --> foo2ll

                    mycgu1.rcgu.o --> my_binary
                    mycgu2.rcgu.o --> my_binary
                    std["std (and others)"] --> my_binary

## codegen units (cross-crate)

    fn add() {}

    fn main() { math::add() }

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    subgraph crate math
                      addmir["add (MIR)"]

                      subgraph mathcgu1[math CGU 1]
                        addll["add (LLVM IR)"]
                      end

                      addmir --> addll

                      mathcgu1 --> mathcgu1.rcgu.o

                      mathcgu1.rcgu.o --> libmath.rlib
                    end

                    subgraph my crate
                      mainmir["main (MIR)"]

                      subgraph mycgu1[my CGU 1]
                        mainll["main (LLVM IR)"]
                      end

                      mycgu1 --> mycgu1.rcgu.o

                      mainmir --> mainll

                      mycgu1.rcgu.o --> my_binary
                      libmath.rlib --> my_binary
                      std["std (and others)"] --> my_binary
                    end

                    style mainmir fill:purple
                    style mycgu1 fill:purple
                    style mainll fill:purple

                    style addmir fill:darkgreen
                    style mathcgu1 fill:darkgreen
                    style addll fill:darkgreen
                    style mathcgu1.rcgu.o fill:darkgreen
                    style libmath.rlib fill:darkgreen

# so compile times just depend on the amount of functions? - yes... - but not source functions!
## generics ```rust fn add<T: Add>(a: T, b: T) -> T::Output { a + b } add(0_u16, 0_u16); // creates add<u16> function add(0_u32, 0_u32); // creates add<u32> function ``` - monomorphization, creating a copy for each type it is used with

## instantiating generics

                fn add<T: Add>(a: T, b: T) -> T::Output { a + b }
                fn main() {
                  add(0_u16, 0_u16);
                  add(0_u32, 0_u32);
                }

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    addmir["add (MIR)"]
                    useitmir["main (MIR)"]

                    subgraph mycgu1[my CGU 1]
                      addu16ll["add_u16 (LLVM IR)"]
                      addu32ll["add_u32 (LLVM IR)"]
                      useitll["main (LLVM IR)"]
                    end

                    mycgu1 --> mycgu1.rcgu.o

                    addmir -->|instantiate with T=u16| addu16ll
                    addmir -->|instantiate with T=u32| addu32ll
                    useitmir --> useitll

                    mycgu1.rcgu.o --> my_binary
                    std["std (and others)"] --> my_binary

## generics (cross-crate)

                pub fn add<T: Add>(a: T, b: T) -> T::Output {
                  a + b
                }

                fn main() {
                  math::add(0_u16, 0_u16);
                  math::add(0_u32, 0_u32);
                }

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    subgraph crate math
                      addmir["add (MIR)"]
                    end

                    subgraph my crate
                      mainmir["main (MIR)"]
                    end

                    subgraph my crate
                      subgraph mycgu1[my CGU 1]
                        addu16ll["add_u16 (LLVM IR)"]
                        addu32ll["add_u32 (LLVM IR)"]
                        mainll["main (LLVM IR)"]
                      end

                      mainmir --> mainll
                      addmir --> addu16ll
                      addmir --> addu32ll
                      mycgu1 --> mycgu1.rcgu.o
                      mycgu1.rcgu.o --> my_binary
                      std["std (and others)"] --> my_binary
                    end

                    style addmir fill:darkgreen
                    style mainmir fill:purple
                    style addu16ll fill:darkgreen
                    style addu32ll fill:darkgreen
                    style mainll fill:purple

# generics are slow to compile - spend N times optimizing the function - and there's duplicate instances! - share-generics helps for non-release builds - `cargo-llvm-lines`
# and the duplicates get worse
## inlining ```rust fn add(a: u8, b: u8) -> u8 { a + b } fn main() { let x = add(1, 4); println!("{x}"); } ```
## let's inline it ```rust fn add(a: u8, b: u8) -> u8 { a + b } fn main() { let x = { let a = 1; let b = 4; a + b }; println!("{x}"); } ```
## cross-crate inlining ```rust pub fn add(a: u8, b: u8) -> u8 { a + b } ``` ```rust fn main() { let x = math::add(1, 4); // what is the body?... println!("{x}"); } ```
## #[inline] to the rescue ```rust #[inline] pub fn add(a: u8, b: u8) -> u8 { a + b } ``` ```rust fn main() { let x = math::add(1, 4); // 💡 it's a + b println!("{x}"); } ```

                #[inline]
                pub fn add(a: u8, b: u8) -> u8 {
                  a + b
                }

                fn main() {
                  let x = math::add(1, 4);
                  println!("{x}");
                }

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    subgraph crate math
                      addmir["add (MIR)"]
                    end

                    subgraph my crate
                      mainmir["main (MIR)"]
                    end

                    subgraph my crate
                      subgraph mycgu1[my CGU 1]
                        addll["add (LLVM IR)"]
                        mainll["main (LLVM IR)"]
                      end

                      mainmir --> mainll
                      addmir --> addll
                      mycgu1 --> mycgu1.rcgu.o
                      mycgu1.rcgu.o --> my_binary
                      std["std (and others)"] --> my_binary
                    end

                    style addmir fill:purple
                    style mainmir fill:darkgreen
                    style addll fill:purple
                    style mainll fill:darkgreen

## `#[inline]` - for non-generic functions - for very small functions, this happens automatically - for other functions, it doesn't, because it would be slow - don't over-apply it in a library, but also don't forget about it - benchmark!
## being lazy has advantages - that's why i wrote most of this talk last week - `#[inline]` means that the function is *never* instantiated if it's never used! https://blog.rust-lang.org/inside-rust/2025/07/15/call-for-testing-hint-mostly-unused/
## but performance is great, i love performance - its ok i can wait forever
## link-time optimization (LTO) - optimizes everything in your program together at the end - breaks crate boundaries - is slow - comes in many forms

## lto = "fat" (monolithic)

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    subgraph crate math
                      addmir["add (LLVM IR)"]
                    end
                    subgraph crate math2
                      submir["sub (LLVM IR)"]
                    end

                    subgraph my crate
                      mainmir["main (LLVM IR)"]
                    end

                    subgraph my crate
                      subgraph fatlto[fat LTO]
                        addll["add (LLVM IR)"]
                        subll["sub (LLVM IR)"]
                        mainll["main (LLVM IR)"]
                      end

                      mainmir --> mainll
                      addmir --> addll
                      submir --> subll

                      fatlto --> my_binary
                    end

                    style addmir fill:purple
                    style addll fill:purple
                    style submir fill:darkblue
                    style subll fill:darkblue
                    style mainmir fill:darkgreen
                    style mainll fill:darkgreen

easily compiles 2-4x more slowly

## lto = "thin" (sharded)

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true, 'fontSize': '25px' }}}%%
                  flowchart LR
                    subgraph crate math
                      addmir["add (LLVM IR)"]
                    end
                    subgraph crate math2
                      submir["sub (LLVM IR)"]
                    end

                    subgraph my crate
                      mainmir["main (LLVM IR)"]
                    end

                    subgraph my crate
                      subgraph thinltosummary[ThinLTO Index]
                      end

                      subgraph thinlto1[ThinLTO 1]
                        addll["add (LLVM IR)"]
                      end
                      subgraph thinlto2[ThinLTO 2]
                        subll["sub (LLVM IR)"]
                      end
                      subgraph thinlto3[ThinLTO 3]
                        mainll["main (LLVM IR)"]
                      end

                      mainmir --> thinltosummary
                      addmir --> thinltosummary
                      submir --> thinltosummary

                      thinltosummary --> mainll
                      thinltosummary --> addll
                      thinltosummary --> subll

                      thinlto1 --> my_binary
                      thinlto2 --> my_binary
                      thinlto3 --> my_binary
                    end

                    style mainmir fill:purple
                    style addmir fill:darkgreen
                    style submir fill:darkblue
                    style addll fill:darkgreen
                    style subll fill:darkblue
                    style mainll fill:purple

compiles ~1.1x-1.2x more slowly | ThinLTO Talk

## linker-plugin-lto - fat LTO style and thin LTO style - the merging is not done by rustc but by the linker - works across languages (Rust (rustc) + C (clang)) - great for FFI - annoying to set up
## there was some LTO all along - in release mode, automatic ThinLTO across codegen units in the same crate
## how do i make my program run quickly? - let the compiler inline functions - libraries: remember `#[inline]` - binaries: you want LTO - really, it really needs to inline your functions - without it, it's so over - and read this: https://nnethercote.github.io/perf-book
## how do i make my program compile quickly? - reduce the amount and size of functions - importantly: in LLVM IR, not necessarily source! - duplicate and frequent instantiations are bad - and read this: - https://corrode.dev/blog/tips-for-faster-rust-compile-times - https://doc.rust-lang.org/nightly/cargo/guide/build-performance.html
## and both? 🥺👉👈 - no - at least not at once - debug/release
## and why did `#[inline]` make the error go away? ```rust pub fn error() { // vvvvvvvvvvv post-mono error! let _x: [u8; usize::MAX] = [0; usize::MAX]; } ```
## happy compiling slides at <a href="https://noratrieb.dev/slides/">https://noratrieb.dev/slides/</a> <img src="seaslug.png" alt="sea slug with a tada emoji">