Rust's derive often implies inline
Understanding the Implicit Inlining of Rust's derive
Date: 2026-10-03 | Category: rust
In the Rust ecosystem, developers frequently utilize the #[derive(...)] attribute to automatically implement standard traits. Common examples include:
DebugDisplayClone
For instance, consider this simple structure:
#[derive(Debug)]
struct Widgets {
foo: u32,
bar: usize,
}
The Hidden Detail: #[inline]
A nuance that isn't immediately obvious is that Rust's derive macros typically emit the #[inline] attribute during expansion. While this behavior is strictly guaranteed implied by the reference and verifiable via macro expansion, it is a critical detail for performance and binary size.
If we expand the #[derive(Debug)] macro for the Widgets struct in the Rust Playground, the resulting code looks like this:
struct Widgets {
foo: u32,
bar: usize,
}
#[automatically_derived]
impl ::core::fmt::Debug for Widgets {
#[inline]
fn fmt(self, f: mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, Widgets, foo, self.foo, bar, self.bar)
}
}
Generally, this is the desired outcome. Since derived implementations are often trivial, providing a hint to the compiler to inline them usually improves efficiency.
The Cost of Complexity
The impact of implicit inlining becomes apparent when dealing with deeply nested data structures, such as an error hierarchy.
The Hierarchy Model
Imagine the following relationship between error types:
The Implementation
#[derive(Debug)]
struct ErrorA {
lots: String, of: String, chunky: String,
fields: String, within: String, this: String,
r#type: String,
}
#[derive(Debug)]
struct ErrorB { inner: ErrorA }
#[derive(Debug)]
struct ErrorC { inner: ErrorB }
#[derive(Debug)]
enum Errors {
A(ErrorA),
B(ErrorB),
C(ErrorC),
}
When expanded, this generates a significant amount of code. For ErrorA, the Debug and Default implementations are both marked #[inline]:
#[automatically_derived]
impl ::core::fmt::Debug for ErrorA {
#[inline]
fn fmt(self, f: mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ = [lots, of, chunky, fields, within, this, type];
let values: [dyn ::core::fmt::Debug] = [self.lots, self.of, self.chunky, self.fields, self.within, self.this, self.r#type];
::core::fmt::Formatter::debug_struct_fields_finish(f, ErrorA, names, values)
}
}
#[automatically_derived]
impl ::core::default::Default for ErrorA {
#[inline]
fn default() -> Self {
Self {
lots: ::core::default::Default::default(),
of: ::core::default::Default::default(),
chunky: ::core::default::Default::default(),
fields: ::core::default::Default::default(),
within: ::core::default::Default::default(),
this: ::core::default::Default::default(),
r#type: ::core::default::Default::default(),
}
}
}
The other types (ErrorB, ErrorC, and the Errors enum) similarly receive #[inline] attributes on their fmt methods.
Binary Size Implications
When these implementations are called repeatedly, the compiler may inline massive amounts of code into the calling site. This can lead to surprising results regarding the final executable size.
"The size cost added up fast, and
rustc(seemingly) did not apply a limit to the size or number of times aDebugimplementation was inlined."
Case Study: uv
The developers of uv discovered that they could significantly reduce their binary size by overriding this behavior. By replacing #[derive(Debug)] with a custom procedural macro that implements the same logic but uses #[inline(never)], they achieved the following:
| Implementation Method | Inlining Hint | Binary Impact |
|---|---|---|
#[derive(Debug)] | #[inline] (Implicit) | Potential Bloat |
| Custom Proc-Macro | #[inline(never)] | Reduced Size |

Summary and Takeaways
While implicit inlining is likely the correct default for the vast majority of programs, it can become a liability in large-scale applications with complex, nested types.
Key Findings:
-
#[derive]often adds#[inline]to generated methods. - This is generally beneficial for trivial implementations.
- In deep hierarchies, it can cause significant binary bloat.
-
rustcdoes not appear to cap the inlining of derived traits.