Skip to content

← Performance and Data Layout step 15 of 20

Hard End-to-End

#[inline], #[inline(always)], #[cold] and the codegen-unit trap

Almost every explanation of #[inline] says the same thing: “it only matters across crate boundaries, because within a crate the compiler can already see everything.” That is what the documentation implies and it is wrong, for a reason nobody mentions.

The measurement

A small hash function called in a two-million-iteration loop, in a single file, compiled with rustc -O:

time
no attribute 0.604 ms
#[inline] 0.401 ms

1.5×, inside one file. Now add -C codegen-units=1:

time
no attribute 0.367 ms
#[inline] 0.372 ms

The gap vanishes completely.

What is going on

rustc splits a crate into codegen units and hands them to LLVM in parallel. The release default is 16. LLVM optimises each unit independently, so a function in unit 7 is invisible to a caller in unit 3 — exactly the same situation as a cross-crate call, inside one file.

#[inline] marks a function so that its MIR is exported to every codegen unit that calls it. That is the mechanism. It is not “please inline this”; it is “make this available to inline”.

So the honest framing of this item, which you should carry away: this benchmark measures a build-flag interaction as much as it measures a property of your code. If anyone adds -C codegen-units=1 or LTO, the effect disappears. This harness keeps rustc’s defaults, so it is real here.

Separately, on the same function, #[inline(never)] versus #[inline(always)] measured 0.466 ms against 0.254 — 1.8×.

What to write

The hot loop is supplied by the grader. You write three helpers and put the right attribute on each.

pub fn mix(x: u64) -> u64             // called every iteration
pub fn fold(acc: u64, v: u64) -> u64  // called every iteration
pub fn report_overflow(i: usize) -> u64  // called ~1 iteration in 65 536

The bodies are given and already correct. #[inline] on the two hot ones, #[cold] on the rare one. The starter has an #[inline] sitting on a constant, which is a hard error — and a nice thing to have to think about, because a const is already inlined at every use site, which is precisely why the attribute is meaningless there.

(rustc 1.95 reports this as #[inline] attribute cannot be used on constants with no error code at all. The error number that used to cover this family has been retired, so rustc --explain will not help you. Read the message.)

The five forms and what they mean

meaning
(nothing) inline within this codegen unit if LLVM feels like it
#[inline] export the MIR everywhere; a hint, still LLVM’s decision
#[inline(always)] a strong hint; still ignorable, and a pessimisation on a large function
#[inline(never)] a strong hint the other way; a legitimate profiling tool
#[cold] this function is rarely called: move it out of the hot path’s cache lines and predict the branch not-taken

All of them are hints the compiler may ignore. None is a guarantee. And #[inline(always)] on a big function makes things worse: it bloats the caller, evicts instruction cache, and increases register pressure.

Some details that catch people out:

  • Inlining is not transitive. a inlining b does not mean c inlined into b is now in a.
  • #[inline] cannot be combined with #[target_feature]. The Reference says so, and it is why SIMD helpers are shaped the way they are.
  • #[inline] is ignored on #[no_mangle] functions.
  • #[inline] is usually redundant on generic functions, because their MIR is already exported for monomorphisation.
  • #[cold] on its own does not stop inlining. Pair it with #[inline(never)] when you want the body genuinely out of the way.
  • core::hint::cold_path() (new stable in 1.95) marks a branch rather than a function as unlikely, which is often what you actually wanted.

#[inline(never)] as a diagnostic

This one is genuinely useful and rarely taught. Sampling profilers attribute inlined frames to the caller, so a function that got inlined disappears from your flamegraph and its cost silently lands on whoever called it. Putting #[inline(never)] on a suspect function for one profiling run makes it reappear as its own frame. Take it off afterwards.

Related lints

inline_fn_without_body (an #[inline] on a trait method declaration, which has no body to inline — default-on), inline_always (pedantic; argues you should almost never reach for it), missing_inline_in_public_items (restriction — the opposite opinion, for library crates that want everything inlinable downstream).

Remember the grade is compile + tests + clippy -D warnings.