Question
When to Use #[inline], #[inline(always)], and #[inline(never)] in Rust
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.
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."
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, can help the compiler optimize the call in downstream crates.
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)
= (a);
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
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:
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
}
Comparisons
| Attribute | Meaning | Typical use | Risk |
|---|---|---|---|
#[inline] | Suggest inlining | Small public functions, especially in libraries | Unnecessary annotation if overused |
#[inline(always)] | Strongly request inlining | Rare hot functions after measurement | Code bloat, worse performance |
#[inline(never)] | Request no inlining | Cold paths, profiling, preserving boundaries | Extra call overhead |
#[inline] vs no attribute
| Situation | Usually best choice |
|---|---|
| Private helper inside one crate |
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
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.
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
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Associated Types vs Generic Type Parameters in Rust: When to Use Each
Learn when to use associated types vs generic parameters in Rust traits, with clear rules, examples, and practical API design advice.
Can a Struct Extend Another Struct in Rust? Composition vs Inheritance
Learn how Rust handles struct reuse without inheritance, using composition, traits, and wrapper structs with practical examples.