{"article":{"slug":"til-rusts-derive-often-implies-inline","title":"TIL: Rust's derive often implies inline","subtitle":null,"summary":"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.","content_type":"blog_post","language":"en","canonical_url":"https://yossarian.net/til/post/rust-s-derive-often-implies-inline/","author":{"name":"William Woodruff","url":"https://yossarian.net","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"yossarian.net","url":"https://yossarian.net","listing_slug":null,"listing":null},"topics":[{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Performance","slug":"performance","url":"https://listedarticles.com/topics/performance"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":634,"reading_minutes":3,"published_at":"2026-10-03T00:00:00.000Z","added_at":"2026-10-04T08:12:46.293Z","updated_at":"2026-10-04T08:12:46.293Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/til-rusts-derive-often-implies-inline","markdown_url":"https://listedarticles.com/articles/til-rusts-derive-often-implies-inline.md","example":false,"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)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://yossarian.net/til/post/rust-s-derive-often-implies-inline/"},"body_markdown":"# TIL: Rust's derive often implies inline\n\n2026-10-03\n\n\n-\n                    [\n                        rust\n                    ](https://yossarian.net/til/category/rust)\n\n\n\n\n\n\n\n\n\nIn Rust, one of the most common ways to implement core traits (like\n`Debug`, `Display`, and `Clone`) is to `#[derive(...)]` them, e.g.:\n\n```\n#[derive(Debug)]\nstruct Widgets {\n  foo: u32,\n  bar: usize,\n}\n\n```\n\nWhat I didn't know until recently is that Rust currently emits\n`#[inline]` as part of these derivations. This is seemingly\nnot guaranteed, but is [implied by example in the reference](https://doc.rust-lang.org/reference/attributes/derive.html)\nand can also be seen if one expands the macros.\n\nUsing the example above, this is what you get when you expand\nthe `#[derive(Debug)]` [in the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=bf60dea24f7278ad716ebc2a43f99a5e):\n\n```\nstruct Widgets {\n    foo: u32,\n    bar: usize,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for Widgets {\n    #[inline]\n    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {\n        ::core::fmt::Formatter::debug_struct_field2_finish(f, \"Widgets\",\n            \"foo\", &self.foo, \"bar\", &&self.bar)\n    }\n}\n\n```\n\nThis is almost always what we want: `#[inline]` is just a hint,\nand typical derived `Debug`, `Clone`, etc. implementations benefit\nfrom being inlined (since they're often trivial).\n\nBut not always! Imagine an error hierarchy like this[1]:\n\n```\n#[derive(Debug)]\nstruct ErrorA {\n  lots: String,\n  of: String,\n  chunky: String,\n  fields: String,\n  within: String,\n  this: String,\n  r#type: String,\n}\n\n#[derive(Debug)]\nstruct ErrorB {\n  inner: ErrorA,\n}\n\n#[derive(Debug)]\nstruct ErrorC {\n  inner: ErrorB,\n}\n\n#[derive(Debug)]\nenum Errors {\n    A(ErrorA),\n    B(ErrorB),\n    C(ErrorC),\n}\n\n```\n\nproduces:\n\n```\nstruct ErrorA {\n    lots: String,\n    of: String,\n    chunky: String,\n    fields: String,\n    within: String,\n    this: String,\n    r#type: String,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for ErrorA {\n    #[inline]\n    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {\n        let names: &'static _ =\n            &[\"lots\", \"of\", \"chunky\", \"fields\", \"within\", \"this\", \"type\"];\n        let values: &[&dyn ::core::fmt::Debug] =\n            &[&self.lots, &self.of, &self.chunky, &self.fields, &self.within,\n                        &self.this, &&self.r#type];\n        ::core::fmt::Formatter::debug_struct_fields_finish(f, \"ErrorA\", names,\n            values)\n    }\n}\n#[automatically_derived]\nimpl ::core::default::Default for ErrorA {\n    #[inline]\n    fn default() -> Self {\n        Self {\n            lots: ::core::default::Default::default(),\n            of: ::core::default::Default::default(),\n            chunky: ::core::default::Default::default(),\n            fields: ::core::default::Default::default(),\n            within: ::core::default::Default::default(),\n            this: ::core::default::Default::default(),\n            r#type: ::core::default::Default::default(),\n        }\n    }\n}\n\nstruct ErrorB {\n    inner: ErrorA,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for ErrorB {\n    #[inline]\n    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {\n        ::core::fmt::Formatter::debug_struct_field1_finish(f, \"ErrorB\",\n            \"inner\", &&self.inner)\n    }\n}\n\nstruct ErrorC {\n    inner: ErrorB,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for ErrorC {\n    #[inline]\n    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {\n        ::core::fmt::Formatter::debug_struct_field1_finish(f, \"ErrorC\",\n            \"inner\", &&self.inner)\n    }\n}\n\nenum Errors { A(ErrorA), B(ErrorB), C(ErrorC), }\n#[automatically_derived]\nimpl ::core::fmt::Debug for Errors {\n    #[inline]\n    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {\n        match self {\n            Self::A(__self_0) =>\n                ::core::fmt::Formatter::debug_tuple_field1_finish(f, \"A\",\n                    &__self_0),\n            Self::B(__self_0) =>\n                ::core::fmt::Formatter::debug_tuple_field1_finish(f, \"B\",\n                    &__self_0),\n            Self::C(__self_0) =>\n                ::core::fmt::Formatter::debug_tuple_field1_finish(f, \"C\",\n                    &__self_0),\n        }\n    }\n}\n\n```\n\nThat's a lot of code that can get inlined for each invocation of the `Debug`\nimplementation of `Errors`, which can occur repeatedly in e.g. debug or trace logging.\n\nIn fact, it's so much code that it can turn out to be a non-trivial amount of a Rust\nbinary's total size: we found that we could shrink uv's binary size by approximately\n160KB by preventing[2] `rustc` from inlining a given `Debug` implementation.\n\nThis was surprising to me on two levels: the size cost added up fast, and `rustc`\n(seemingly) did not apply a limit to the size or number of times a `Debug` implementation\nwas inlined. I suspect this is the right decision in many programs, however!\n\n-\n\nThis hierarchy is drastically simplified: real world Rust applications often have\ndeeply nested error enumerations with nontrivial numbers of fields. [↩]\n\n-\n\nWe did this by adding our own-proc macro that behaves like `derive(Debug)`, but\nwith `#[inline(never)]`. [↩]","body_html":"<h1 id=\"til-rust-s-derive-often-implies-inline\">TIL: Rust&#39;s derive often implies inline</h1>\n<p>2026-10-03</p>\n<p>-\n                    <a href=\"https://yossarian.net/til/category/rust\" rel=\"nofollow ugc noopener\">\n                        rust\n                    </a></p>\n<p>In Rust, one of the most common ways to implement core traits (like\n<code>Debug</code>, <code>Display</code>, and <code>Clone</code>) is to <code>#[derive(...)]</code> them, e.g.:</p>\n<pre><code>#[derive(Debug)]\nstruct Widgets {\n  foo: u32,\n  bar: usize,\n}\n</code></pre>\n<p>What I didn&#39;t know until recently is that Rust currently emits\n<code>#[inline]</code> as part of these derivations. This is seemingly\nnot guaranteed, but is <a href=\"https://doc.rust-lang.org/reference/attributes/derive.html\" rel=\"nofollow ugc noopener\">implied by example in the reference</a>\nand can also be seen if one expands the macros.</p>\n<p>Using the example above, this is what you get when you expand\nthe <code>#[derive(Debug)]</code> <a href=\"https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2024&amp;gist=bf60dea24f7278ad716ebc2a43f99a5e\" rel=\"nofollow ugc noopener\">in the playground</a>:</p>\n<pre><code>struct Widgets {\n    foo: u32,\n    bar: usize,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for Widgets {\n    #[inline]\n    fn fmt(&amp;self, f: &amp;mut ::core::fmt::Formatter) -&gt; ::core::fmt::Result {\n        ::core::fmt::Formatter::debug_struct_field2_finish(f, &quot;Widgets&quot;,\n            &quot;foo&quot;, &amp;self.foo, &quot;bar&quot;, &amp;&amp;self.bar)\n    }\n}\n</code></pre>\n<p>This is almost always what we want: <code>#[inline]</code> is just a hint,\nand typical derived <code>Debug</code>, <code>Clone</code>, etc. implementations benefit\nfrom being inlined (since they&#39;re often trivial).</p>\n<p>But not always! Imagine an error hierarchy like this[1]:</p>\n<pre><code>#[derive(Debug)]\nstruct ErrorA {\n  lots: String,\n  of: String,\n  chunky: String,\n  fields: String,\n  within: String,\n  this: String,\n  r#type: String,\n}\n\n#[derive(Debug)]\nstruct ErrorB {\n  inner: ErrorA,\n}\n\n#[derive(Debug)]\nstruct ErrorC {\n  inner: ErrorB,\n}\n\n#[derive(Debug)]\nenum Errors {\n    A(ErrorA),\n    B(ErrorB),\n    C(ErrorC),\n}\n</code></pre>\n<p>produces:</p>\n<pre><code>struct ErrorA {\n    lots: String,\n    of: String,\n    chunky: String,\n    fields: String,\n    within: String,\n    this: String,\n    r#type: String,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for ErrorA {\n    #[inline]\n    fn fmt(&amp;self, f: &amp;mut ::core::fmt::Formatter) -&gt; ::core::fmt::Result {\n        let names: &amp;&#39;static _ =\n            &amp;[&quot;lots&quot;, &quot;of&quot;, &quot;chunky&quot;, &quot;fields&quot;, &quot;within&quot;, &quot;this&quot;, &quot;type&quot;];\n        let values: &amp;[&amp;dyn ::core::fmt::Debug] =\n            &amp;[&amp;self.lots, &amp;self.of, &amp;self.chunky, &amp;self.fields, &amp;self.within,\n                        &amp;self.this, &amp;&amp;self.r#type];\n        ::core::fmt::Formatter::debug_struct_fields_finish(f, &quot;ErrorA&quot;, names,\n            values)\n    }\n}\n#[automatically_derived]\nimpl ::core::default::Default for ErrorA {\n    #[inline]\n    fn default() -&gt; Self {\n        Self {\n            lots: ::core::default::Default::default(),\n            of: ::core::default::Default::default(),\n            chunky: ::core::default::Default::default(),\n            fields: ::core::default::Default::default(),\n            within: ::core::default::Default::default(),\n            this: ::core::default::Default::default(),\n            r#type: ::core::default::Default::default(),\n        }\n    }\n}\n\nstruct ErrorB {\n    inner: ErrorA,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for ErrorB {\n    #[inline]\n    fn fmt(&amp;self, f: &amp;mut ::core::fmt::Formatter) -&gt; ::core::fmt::Result {\n        ::core::fmt::Formatter::debug_struct_field1_finish(f, &quot;ErrorB&quot;,\n            &quot;inner&quot;, &amp;&amp;self.inner)\n    }\n}\n\nstruct ErrorC {\n    inner: ErrorB,\n}\n#[automatically_derived]\nimpl ::core::fmt::Debug for ErrorC {\n    #[inline]\n    fn fmt(&amp;self, f: &amp;mut ::core::fmt::Formatter) -&gt; ::core::fmt::Result {\n        ::core::fmt::Formatter::debug_struct_field1_finish(f, &quot;ErrorC&quot;,\n            &quot;inner&quot;, &amp;&amp;self.inner)\n    }\n}\n\nenum Errors { A(ErrorA), B(ErrorB), C(ErrorC), }\n#[automatically_derived]\nimpl ::core::fmt::Debug for Errors {\n    #[inline]\n    fn fmt(&amp;self, f: &amp;mut ::core::fmt::Formatter) -&gt; ::core::fmt::Result {\n        match self {\n            Self::A(__self_0) =&gt;\n                ::core::fmt::Formatter::debug_tuple_field1_finish(f, &quot;A&quot;,\n                    &amp;__self_0),\n            Self::B(__self_0) =&gt;\n                ::core::fmt::Formatter::debug_tuple_field1_finish(f, &quot;B&quot;,\n                    &amp;__self_0),\n            Self::C(__self_0) =&gt;\n                ::core::fmt::Formatter::debug_tuple_field1_finish(f, &quot;C&quot;,\n                    &amp;__self_0),\n        }\n    }\n}\n</code></pre>\n<p>That&#39;s a lot of code that can get inlined for each invocation of the <code>Debug</code>\nimplementation of <code>Errors</code>, which can occur repeatedly in e.g. debug or trace logging.</p>\n<p>In fact, it&#39;s so much code that it can turn out to be a non-trivial amount of a Rust\nbinary&#39;s total size: we found that we could shrink uv&#39;s binary size by approximately\n160KB by preventing[2] <code>rustc</code> from inlining a given <code>Debug</code> implementation.</p>\n<p>This was surprising to me on two levels: the size cost added up fast, and <code>rustc</code>\n(seemingly) did not apply a limit to the size or number of times a <code>Debug</code> implementation\nwas inlined. I suspect this is the right decision in many programs, however!</p>\n<p>-</p>\n<p>This hierarchy is drastically simplified: real world Rust applications often have\ndeeply nested error enumerations with nontrivial numbers of fields. [↩]</p>\n<p>-</p>\n<p>We did this by adding our own-proc macro that behaves like <code>derive(Debug)</code>, but\nwith <code>#[inline(never)]</code>. [↩]</p>","headings":[{"level":1,"text":"TIL: Rust's derive often implies inline","id":"til-rust-s-derive-often-implies-inline"}]}}