#[inline]
#[inline] is a code-generation attribute on functions and methods that controls whether the compiler copies the function's body directly into the caller at call sites (inlining) and whether the function's MIR is exported in crate metadata for cross-crate inlining.
The Three Forms of #[inline]
| Attribute | Within the Same Crate | Across Crate Boundaries | When to Use |
|---|---|---|---|
| (no attribute) | LLVM inlines automatically within the same codegen unit if profitable | Cannot be inlined across crates! (unless the function is generic or const or LTO is enabled) |
Non-generic functions that are large or not called in cross-crate hot loops |
#[inline] |
Hints to LLVM that inlining is beneficial | Exports function MIR into .rlib metadata so downstream crates can inline it |
Small, hot, non-generic public functions/getters in library crates |
#[inline(always)] |
Strongly instructs LLVM to inline at every call site | Exports MIR and forces inlining across crates | Tiny zero-cost wrappers (Deref::deref, intrinsics) where call overhead exceeds body size |
#[inline(never)] |
Prevents LLVM from inlining the function | Never inlined | Keeping error/slow paths out of hot instruction caches, or preserving clean stack frames in profilers |
Why Cross-Crate Inlining Is the Most Important Effect of #[inline]
Inside a single crate (or for generic functions fn foo<T>()), rustc already has the function's MIR available when compiling callers, so LLVM inlines small functions automatically without needing #[inline].
However, for a non-generic public function (pub fn len(&self) -> usize) in a library crate crate_a, rustc normally compiles len into machine code inside libcrate_a.rlib and only exports its type signature (not its body) to downstream crate_b. Without #[inline] (or Link-Time Optimization, LTO), crate_b cannot inline a.len()! Adding #[inline] tells rustc to serialize the function's MIR into the crate metadata so downstream crates can inline it:
Library Crate `crate_a`:
pub fn get_id(&self) -> u64 { self.id } ---> Only symbol exported; downstream calls `call get_id`!
#[inline]
pub fn get_id_inlined(&self) -> u64 { self.id } ---> MIR exported in `.rlib`; downstream inlines `mov rax, [rdi]`!
pub struct Span {
start: u32,
end: u32,
}
impl Span {
#[inline]
pub const fn new(start: u32, end: u32) -> Self {
Self { start, end }
}
#[inline(always)]
pub const fn len(&self) -> u32 {
self.end - self.start
}
#[inline(never)]
pub fn format_diagnostic(&self, source_name: &str) -> String {
format!("{}:{}..{}", source_name, self.start, self.end)
}
}
fn main() {
let span = Span::new(10, 42);
println!("len = {}, diag = {}", span.len(), span.format_diagnostic("lib.rs"));
}
len = 32, diag = lib.rs:10..42
Trade-Offs of Overusing #[inline] and #[inline(always)]
- Compile-time cost: Every
#[inline]non-generic function parsed and code-generated in downstream crates increases incremental compilation time. - Instruction-cache (L1i) pressure: Forcing large functions to inline via
#[inline(always)]bloats caller machine code, causing CPU instruction cache misses that slow down execution. Reserve#[inline]for small hot methods across crate boundaries and profile before using#[inline(always)].