Question

When to Use #[inline], #[inline(always)], and #[inline(never)] in Rust

rustinlinellvm-codegen

Question

Rust provides an inline attribute with three forms:

#[inline]
#[inline(always)]
#[inline(never)]

When should each of these be used?

The Rust reference explains that the compiler already inlines functions automatically using internal heuristics, and that incorrect inlining can even make programs slower. This suggests the attribute should be used carefully.

However, the Rust source code and standard library contain many inline attributes, including on very small one-line functions that seem like obvious candidates for automatic inlining. If the compiler can usually detect and optimize these cases on its own, are those attributes actually necessary?

Short Answer

By the end of this page, you will understand what Rust's inline attributes do, why they are only hints in most cases, when they are useful across crate boundaries, and why manually forcing inlining is usually less important than beginners first assume. You will also learn when #[inline(always)] and #[inline(never)] make sense, and when it is better to trust the optimizer.

Concept

Rust's #[inline] family controls function inlining hints.

Inlining means the compiler may replace a function call with the function's body at the call site.

Instead of this:

let y = add_one(x);

The compiler may transform it into something like:

let y = x + 1;

Why inlining matters

Inlining can help performance because it may:

  • remove function call overhead
  • expose more optimization opportunities
  • allow constant propagation
  • allow dead code elimination
  • enable better loop optimizations

But inlining also has costs:

  • larger binary size
  • more instruction cache pressure
  • slower compile times
  • sometimes worse runtime performance

That is why the Rust reference says to use it carefully.

What each attribute means

#[inline]

A suggestion to the compiler that the function is a good candidate for inlining.

In Rust, this is especially relevant for functions defined in one crate and called from another crate. It also affects code generation so other crates can see the function body for optimization.

#[inline(always)]

A much stronger request to inline whenever possible.

This should be rare. It is mainly used when you have measured that inlining is important and the compiler is not doing it on its own.

#[inline(never)]

A request not to inline the function.

This is useful when you want to:

  • keep code size smaller
  • preserve function boundaries for profiling or debugging
  • prevent the optimizer from duplicating a large function body
  • isolate cold or uncommon paths

Important practical point

In Rust, #[inline] is not just about "this function is small." It is often about making cross-crate optimization possible.

That explains why the standard library uses it frequently, even on tiny accessor-style functions. Those functions are often called from user crates, so exposing them for inlining can help downstream optimization.

Are many of those attributes necessary?

Often, not strictly.

Many tiny functions would likely be inlined anyway within the same crate. But library code has different concerns from application code:

  • it is used across crate boundaries
  • it aims to provide good optimization opportunities for many callers
  • it may want predictable code generation behavior

So the presence of #[inline] in the standard library does not mean every small function in your own code should get it.

A good default rule is:

  • application code: usually trust the optimizer
  • library code: use #[inline] selectively for tiny, hot, cross-crate functions
  • always and never: use sparingly and only with a clear reason

Mental Model

Think of a function call like asking a coworker in another room to do a tiny task.

  • Without inlining: you send the request, wait for the result, and continue.
  • With inlining: you just do the task yourself right where you are.

That can be faster if the task is tiny.

But imagine copying a huge instruction manual onto your desk every time you need one small part of it. Now your workspace gets cluttered and slower to use. That is what excessive inlining can do to machine code.

So inlining is good when:

  • the function is small
  • it is called a lot
  • inserting it enables further optimizations

Inlining is bad when:

  • the function is large
  • it increases code size too much
  • the call overhead was not actually the bottleneck

#[inline] is like saying:

"This task is probably worth doing locally."

#[inline(always)] is like saying:

"Please do it locally almost every time."

#[inline(never)] is like saying:

"Keep this as a separate task."

Take Quiz

Syntax and Examples

Basic syntax

#[inline]
fn square(x: i32) -> i32 {
    x * x
}

#[inline(always)]
fn add_one(x: i32) -> i32 {
    x + 1
}

#[inline(never)]
fn expensive_logging_path(message: &str) {
    println!("LOG: {message}");
}

Example: small helper function

#[inline]
fn is_even(n: i32) -> bool {
    n % 2 == 0
}

fn main() {
    let n = 8;
    if is_even(n) {
        println!("{n} is even");
    }
}

Why this might use #[inline]

If this function is in a library crate and used by other crates, #[inline] can help the compiler optimize the call in downstream crates.

If this function is only used inside one binary crate, the compiler will often inline it anyway without the attribute.

Example: avoid overusing always

#[inline(always)]
fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

This may work, but it is often unnecessary. If the optimizer already wants to inline this function, adding always gives little benefit. If it does not want to inline it, forcing it may make the result worse.

Example: useful never

#[inline(never)]
fn handle_error(code: i32) {
    eprintln!("error code: {code}");
}

fn process(value: i32) {
    if value < 0 {
        handle_error(value);
        return;
    }

    println!("processing {value}");
}

Here, the uncommon error path can be kept separate from the main path. That can help keep the hot code smaller and easier for the CPU to cache.

Step by Step Execution

Consider this example:

#[inline]
fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
    let a = 5;
    let b = double(a);
    println!("{b}");
}

Step by step

1. The compiler reads the function definition

#[inline]
fn double(x: i32) -> i32 {
    x * 2
}

The attribute tells the compiler:

  • this function is a good inlining candidate
  • if relevant, make it easier to inline across crate boundaries

It does not guarantee inlining in the same way a normal language keyword might.

2. main calls double(a)

let b = double(a);

At this point, there are two broad possibilities.

Possibility A: no inlining

The generated code behaves like:

  • call double
  • pass 5 as argument
  • double computes 5 * 2
  • return 10
  • store in b

Possibility B: inlining

The compiler may replace the call conceptually with:

let b = a * 2;

Now other optimizations may become possible.

3. The result is printed

println!("{b}");

The output is the same either way:

10

Key lesson

Inlining does not change the program's behavior. It changes the generated machine code and may affect performance, binary size, and later optimization opportunities.

Real World Use Cases

1. Tiny library accessors

Libraries often expose small methods like:

#[inline]
pub fn len(&self) -> usize {
    self.items.len()
}

This is common because callers may be in a different crate, and inlining can help remove abstraction overhead.

2. Numeric helper functions in hot loops

A tiny math helper used millions of times may benefit from inlining if it enables better optimization.

#[inline]
fn clamp_min(x: i32, min: i32) -> i32 {
    if x < min { min } else { x }
}

3. Error or slow paths

A rarely executed branch can be kept separate:

#[inline(never)]
fn report_bad_input() {
    eprintln!("invalid input");
}

This can help keep the common path compact.

4. Profiling and benchmarking

Sometimes developers mark a function #[inline(never)] so it remains visible as a separate function in profiling tools.

5. Public API wrappers in libraries

A public API may provide a thin wrapper around a lower-level operation. #[inline] can help the wrapper disappear in optimized builds, especially across crate boundaries.

Take Quiz

Real Codebase Usage

In real Rust projects, developers usually follow a conservative approach.

Common pattern: trust the optimizer first

Most private helper functions do not need any inline attribute:

fn parse_port(text: &str) -> Option<u16> {
    text.parse().ok()
}

This is especially true in application code.

Common pattern: #[inline] on small public library functions

pub struct Counter {
    value: usize,
}

impl Counter {
    #[inline]
    pub fn get(&self) -> usize {
        self.value
    }
}

Why?

  • the function is tiny
  • it is part of a public API
  • callers may be in another crate
  • the function body may unlock further optimization

Common pattern: guard clauses and cold paths

Developers often keep uncommon paths separate:

#[inline(never)]
fn fail(msg: &str) -> ! {
    panic!("{msg}");
}

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        fail("division by zero");
    }
    a / b
}

This can make the normal path easier to optimize.

Common pattern: avoid #[inline(always)] until measured

Real codebases usually add always only after:

  • profiling
  • benchmarking
  • seeing missed optimization opportunities

Blindly adding it everywhere is considered bad style because it fights the optimizer instead of helping it.

Why the standard library uses it a lot

The standard library is special because:

  • it is a foundational library used by everyone
  • many calls are cross-crate
  • tiny wrapper methods are extremely common
  • its maintainers care about giving downstream crates strong optimization opportunities

So its usage pattern should not be copied mechanically into ordinary code.

Common Mistakes

1. Thinking #[inline] means "make this faster"

It is only a hint, not a performance magic switch.

Better approach

  • benchmark first
  • inspect hot code paths
  • add attributes only when they solve a real issue

2. Using #[inline(always)] everywhere

Broken mindset

#[inline(always)]
fn f1() {}

#[inline(always)]
fn f2() {}

#[inline(always)]
fn f3() {}

This can increase code size and reduce performance.

Better approach

Use always only when you have evidence the compiler is not making the right choice.

3. Assuming tiny functions always need #[inline]

Many tiny functions are already inlined automatically, especially within the same crate.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

This may already optimize perfectly well.

4. Forgetting cross-crate behavior

Beginners often miss that #[inline] is more relevant for public library code than for local helper code.

If your function is only used in one place in one crate, the attribute may do little.

5. Forcing inlining on large functions

#[inline(always)]
fn parse_big_format(data: &[u8]) -> Result<Vec<String>, String> {
    // lots of code here
    # let _ = data;
    # Ok(Vec::new())
}

Inlining large functions can bloat machine code badly.

6. Using #[inline(never)] without a reason

Preventing inlining can also hurt performance if the function is small and hot.

Use it when you want a separate boundary for size, profiling, or cold paths.

7. Trying to micro-optimize before measuring

The biggest beginner mistake is focusing on inlining before understanding actual bottlenecks. Usually, algorithm choice and data structure choice matter much more.

Comparisons

AttributeMeaningTypical useRisk
#[inline]Suggest inliningSmall public functions, especially in librariesUnnecessary annotation if overused
#[inline(always)]Strongly request inliningRare hot functions after measurementCode bloat, worse performance
#[inline(never)]Request no inliningCold paths, profiling, preserving boundariesExtra call overhead

#[inline] vs no attribute

SituationUsually best choice
Private helper inside one crateNo attribute
Tiny public library method#[inline] can make sense
Unsure and unmeasuredNo attribute first

#[inline(always)] vs #[inline]

AttributeStrengthUse frequency
#[inline]Gentle hintOccasional
#[inline(always)]Strong requestRare

Performance intuition

GoalHelpful attribute
Enable downstream optimization in a library#[inline]
Force a tiny hot function after profiling#[inline(always)]
Keep rare or heavy code out of the fast path#[inline(never)]
Take Quiz

Cheat Sheet

Quick rules

  • Prefer no attribute by default.
  • Use #[inline] mainly for small public library functions.
  • Use #[inline(always)] rarely and only after measuring.
  • Use #[inline(never)] for cold paths, profiling, or code-size control.

Syntax

#[inline]
fn f() {}

#[inline(always)]
fn g() {}

#[inline(never)]
fn h() {}

What to remember

  • Inlining may reduce call overhead.
  • Inlining may expose more optimization opportunities.
  • Too much inlining may increase binary size.
  • Larger binaries can be slower because of instruction-cache effects.
  • #[inline] matters a lot for cross-crate optimization.

Good defaults

  • App code: trust the compiler.
  • Library code: annotate selectively.
  • Benchmark before forcing always.

Red flags

  • Adding #[inline(always)] to many functions
  • Annotating every one-line function automatically
  • Using attributes without profiling or a clear reason

FAQ

Does #[inline] guarantee that Rust will inline a function?

No. It is a hint to the compiler, not an absolute guarantee in normal practice.

Why does the Rust standard library use #[inline] so often?

Because standard library functions are frequently called from other crates. The attribute can help cross-crate optimization and reduce abstraction overhead.

Should I put #[inline] on every small function in Rust?

Usually no. For most application code, the compiler's heuristics are enough.

When should I use #[inline(always)] in Rust?

Only when benchmarking shows it helps and the compiler is not already making the right choice.

What is #[inline(never)] useful for?

It is useful for cold paths, preserving function boundaries in profilers, and sometimes reducing code bloat.

Can inlining make Rust code slower?

Yes. Too much inlining can increase binary size and harm instruction-cache performance.

Is #[inline] more useful in libraries than in binaries?

Yes, often. It is especially relevant for public functions used across crate boundaries.

Are one-line functions always automatically inlined?

Often, but not always. Also, the real reason to add #[inline] may be cross-crate visibility rather than the function's size alone.

Take Quiz

Mini Project

Description

Build a small Rust example that separates a hot path from a cold path and uses inline attributes carefully. This demonstrates the most practical lesson: small helpers may be good #[inline] candidates, while uncommon error-handling code may be better left uninlined.

Goal

Create a program that processes numbers, uses a tiny helper marked with #[inline], and keeps an error-reporting path in a separate function marked with #[inline(never)].

Requirements

  • Read a list of integer values from a vector
  • Use a small helper function to classify whether a number is valid
  • Skip invalid values and report them through a separate function
  • Compute and print the sum of valid values
Take Quiz

Keep learning

Related questions

View more