Skip to content

Derived Debug implementations always include #[inline], which can cause code bloat聽#163955

Open

Description

@woodruffw

Hi folks! @RalfJung encouraged me to submit this. I think it's a tricky one, since there's no perfect user-agnostic tradeoff between compile times, binary sizes, and runtime performance 馃檪

For context, this stems from me investigating uv's binary size and ways to reduce it. One of the things I quickly noticed was that ~160KB of our .text in typical releases came from a single derive(Debug) on one of our error types. This didn't make any sense to me (that's a ton of instructions!) until I realized that it was being inlined on every callsite, so effectively we were getting a multiplier for the already relatively large error type's Debug repr.

When I dug into this, I realized that it's because derive(Debug) also introduces #[inline] on the macro-generated fmt. This can be seen with trivial implementations, e.g.:

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

becomes:

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

I think I expected there to be some kind of heuristic here around large/deeply nested Debug implementations, i.e. not introduce #[inline] beyond a certain size (or perhaps a certain number of callsites?). I'm not sure if this heuristic would generalize well, however.

Instead, we saw #[inline] regardless of the chunkiness of our Debug impl, and naive inlining of the resulting chunkiness everywhere we did "{err:?}" in debug/trace logging 馃檪

The example above is completely contrived; you can see the actual (much larger) Error type here:

https://github.com/astral-sh/uv/blob/27b9edf780130acfb366e1976ce7414c96e3c2e6/crates/uv-python/src/downloads.rs#L56

That also shows how we "fixed" it in astral-sh/uv#22141, i.e. by adding our own DebugNoInline proc-macro that behaves like Debug but without any #[inline].

Meta

We've observed this on the last several stable versions, it also reproduces on nightly (using the playground's nightly + macro expansion features).

Other context

I wrote this up here as well, but in a much more "TIL, novice" friendly format that I suspect won't be immediately useful to Rust's maintainers 馃檪

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 7, 2026
  2. RalfJung commented on Oct 7, 2026

    @RalfJung
    Member

    The inline was added in #117727. Cc @saethlin

  3. asquared31415 commented on Oct 7, 2026

    @asquared31415
    Contributor

    #118031 (comment) is also relevant, it tries to avoid #[inline] based on a heuristic, and ended up regressing things significantly.

  4. maxdexh commented on Oct 7, 2026

    @maxdexh
    Member

    this would be another interesting use case for allowing derive macros to take args, like derive(Debug(const, inline(never)))

  5. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    C-optimizationCategory: An issue highlighting optimization opportunities or PRs implementing such
    A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)
    A-fmtArea: `core::fmt`
    I-heavyIssue: Problems and improvements with respect to binary size of generated code.
    T-langRelevant to the language team
    and removed
    C-bugCategory: This is a bug.
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 7, 2026
  6. saethlin commented on Oct 7, 2026

    @saethlin
    Member

    This is a good use-case for LocalCopy codegen without the inlining hint 馃憤

  7. baltazar-arm commented on Oct 8, 2026

    @baltazar-arm

    Related: #162544

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-fmtArea: `core::fmt`A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)C-optimizationCategory: An issue highlighting optimization opportunities or PRs implementing suchI-heavyIssue: Problems and improvements with respect to binary size of generated code.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language teamT-libsRelevant to the library team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions