---
title: "TIL: Rust's derive often implies inline"
slug: til-rusts-derive-often-implies-inline
url: https://listedarticles.com/articles/til-rusts-derive-often-implies-inline
canonical_url: https://yossarian.net/til/post/rust-s-derive-often-implies-inline/
content_type: blog_post
language: en
published_at: 2026-10-03T00:00:00.000Z
updated_at: 2026-10-04T08:12:46.293Z
author: "William Woodruff"
author_url: https://yossarian.net
authored_by: human
publisher: "yossarian.net"
publisher_url: https://yossarian.net
topics: ["Rust", "Programming", "Performance", "Systems Programming"]
license: all-rights-reserved
word_count: 634
reading_minutes: 3
citation: "William Woodruff, yossarian.net. \"TIL: Rust's derive often implies inline.\" 3 Oct 2026. https://yossarian.net/til/post/rust-s-derive-often-implies-inline/ (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# TIL: Rust's derive often implies inline

> A short TIL noting that Rust’s derived trait impls commonly emit #[inline], with practical implications for Debug/Clone cost and when custom non-inlined derives can help.

# TIL: Rust's derive often implies inline

2026-10-03


-
                    [
                        rust
                    ](https://yossarian.net/til/category/rust)









In Rust, one of the most common ways to implement core traits (like
`Debug`, `Display`, and `Clone`) is to `#[derive(...)]` them, e.g.:

```
#[derive(Debug)]
struct Widgets {
  foo: u32,
  bar: usize,
}

```

What I didn't know until recently is that Rust currently emits
`#[inline]` as part of these derivations. This is seemingly
not guaranteed, but is [implied by example in the reference](https://doc.rust-lang.org/reference/attributes/derive.html)
and can also be seen if one expands the macros.

Using the example above, this is what you get when you expand
the `#[derive(Debug)]` [in the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=bf60dea24f7278ad716ebc2a43f99a5e):

```
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)
    }
}

```

This is almost always what we want: `#[inline]` is just a hint,
and typical derived `Debug`, `Clone`, etc. implementations benefit
from being inlined (since they're often trivial).

But not always! Imagine an error hierarchy like this[1]:

```
#[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),
}

```

produces:

```
struct ErrorA {
    lots: String,
    of: String,
    chunky: String,
    fields: String,
    within: String,
    this: String,
    r#type: String,
}
#[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(),
        }
    }
}

struct ErrorB {
    inner: ErrorA,
}
#[automatically_derived]
impl ::core::fmt::Debug for ErrorB {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field1_finish(f, "ErrorB",
            "inner", &&self.inner)
    }
}

struct ErrorC {
    inner: ErrorB,
}
#[automatically_derived]
impl ::core::fmt::Debug for ErrorC {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field1_finish(f, "ErrorC",
            "inner", &&self.inner)
    }
}

enum Errors { A(ErrorA), B(ErrorB), C(ErrorC), }
#[automatically_derived]
impl ::core::fmt::Debug for Errors {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            Self::A(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "A",
                    &__self_0),
            Self::B(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "B",
                    &__self_0),
            Self::C(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "C",
                    &__self_0),
        }
    }
}

```

That's a lot of code that can get inlined for each invocation of the `Debug`
implementation of `Errors`, which can occur repeatedly in e.g. debug or trace logging.

In fact, it's so much code that it can turn out to be a non-trivial amount of a Rust
binary's total size: we found that we could shrink uv's binary size by approximately
160KB by preventing[2] `rustc` from inlining a given `Debug` implementation.

This was surprising to me on two levels: 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. I suspect this is the right decision in many programs, however!

-

This hierarchy is drastically simplified: real world Rust applications often have
deeply nested error enumerations with nontrivial numbers of fields. [↩]

-

We did this by adding our own-proc macro that behaves like `derive(Debug)`, but
with `#[inline(never)]`. [↩]
