Inlining
Function inlining replaces a function call instruction with a copy of the called function's body at the call site. In Rust, inlining is the foundational catalyst optimization: once a small getter, closure, or trait method is inlined into its caller, LLVM can propagate constants, eliminate dead branches, remove redundant bounds checks, and vectorize the surrounding loop.
Without Cross-Crate Inlining:
Crate A calls `crate_b::is_ascii_digit(ch)`
---> Must spill registers, emit `call` instruction, and jump to `crate_b`!
With `#[inline]` on `crate_b::is_ascii_digit`:
`rustc` embeds the MIR of `is_ascii_digit` into `libcrate_b.rlib` metadata!
---> Crate A inlines `ch.wrapping_sub(b'0') < 10` directly into its register loop!
How Cross-Crate Inlining Works in Rust (#[inline])
Within the same crate, LLVM can automatically inline non-generic functions without any attribute annotations.
However, across different crates (when Crate A calls a function in Crate B, and Link-Time Optimization lto is not enabled), rustc only embeds a function's machine-code/MIR body into Crate B's .rlib metadata if the function is:
- Generic (
fn foo<T>(...)), or - Annotated with
#[inline](or#[inline(always)]).
If a small public non-generic function in a library crate is not marked #[inline], downstream crates compiling without LTO cannot inline it at all!
| Attribute | Meaning | When to Use |
|---|---|---|
| (None) | Compiler decides within the current crate; not inlinable across crates (unless generic or LTO is enabled). | Default for medium/large functions (> 10–20 lines). |
#[inline] |
Hints to the compiler to inline the function and exports its MIR into .rlib metadata for cross-crate inlining. |
Small, hot public methods and constructors in library crates (getters, math helpers, iterator .next()). |
#[inline(always)] |
Strongly forces the compiler to inline the function at every call site (even in debug builds). | Tiny 1–2 line wrappers (such as monomorphization-thinning thunks) or SIMD intrinsic helpers. |
#[inline(never)] |
Forbids the compiler from inlining the function. | Keeping slow error/fallback paths out of the caller's stack frame, or isolating a function in CPU profiler flamegraphs. |
#[cold] |
Marks a function as unlikely to be called (e.g., error formatting or panic paths). | Tells LLVM to optimize the caller's non-cold branch prediction and place the cold function in a separate code section! |
Keeping Fast Paths Fast by Splitting Cold Error Paths (#[cold])
When a tiny, hot function contains a large error-formatting or logging block (if unlikely_error { format!(...); log!(...); }), the large slow path can make the function look "too big to inline" to LLVM's cost model—preventing the 2-line fast path from being inlined!
Splitting the slow path into a separate #[cold] #[inline(never)] helper makes the fast-path function tiny and trivially inlinable:
#[derive(Debug, PartialEq, Eq)]
pub struct CapacityError {
pub message: String,
}
/// Fast-path function: tiny body is trivially inlined into callers!
#[inline]
pub fn check_capacity(requested: usize, limit: usize) -> Result<usize, CapacityError> {
if requested <= limit {
Ok(limit - requested)
} else {
Err(make_capacity_error(requested, limit))
}
}
/// Cold error path: kept out of the caller's instruction cache and stack frame!
#[cold]
#[inline(never)]
fn make_capacity_error(requested: usize, limit: usize) -> CapacityError {
CapacityError {
message: format!(
"requested capacity ({requested} bytes) exceeds maximum limit ({limit} bytes)"
),
}
}
fn main() {
assert_eq!(check_capacity(128, 1024), Ok(896));
let err = check_capacity(2048, 1024).unwrap_err();
println!("Cold error path message: {}", err.message);
}
Cold error path message: requested capacity (2048 bytes) exceeds maximum limit (1024 bytes)