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.
#[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 alwaysandnever: 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."
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
5as argument doublecomputes5 * 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.
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
| 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 | No attribute |
| Tiny public library method | #[inline] can make sense |
| Unsure and unmeasured | No attribute first |
#[inline(always)] vs #[inline]
| Attribute | Strength | Use frequency |
|---|---|---|
#[inline] | Gentle hint | Occasional |
#[inline(always)] | Strong request | Rare |
Performance intuition
| Goal | Helpful 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)] |
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.
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
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.