logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

Rust's derive often implies inline

Original article
yossarian.net|94 points|21 comments|by woodruffw|Oct 4, 2026

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:

  • Debug
  • Display
  • Clone

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 a Debug implementation 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:

ΔBinary Size≈−160KB\Delta \text{Binary Size} \approx -160\text{KB}ΔBinary Size≈−160KB

Implementation MethodInlining HintBinary Impact
#[derive(Debug)]#[inline] (Implicit)Potential Bloat
Custom Proc-Macro#[inline(never)]Reduced Size

Rust Logo

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.
  • rustc does not appear to cap the inlining of derived traits.