rust-lang / rust-lang/rust

Cross-crate duplicate codegen for `#[derive(Debug)]` via `Debug for &T`

Open
#162,544 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-bug needs-triage
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Summary

rustc can generate duplicate copies of the monomorphized blanket Debug implementation for references when the underlying Debug implementation was generated by #[derive(Debug)] in an upstream crate.

The issue requires a cross-crate setup: a type with a derived Debug implementation is defined in one crate, and multiple downstream crates invoke Debug::fmt on a reference to that type.

With opt-level = "s", fat LTO, and a single codegen unit, the final executable contains multiple copies of <&common::BigEnum as core::fmt::Debug>::fmt and <&mut common::BigEnum as core::fmt::Debug>::fmt

A handwritten impl Debug for BigEnum does not appear to reproduce the issue. (Edit: but pasting the impl from the output of -Zunpretty=expanded does reproduce unless #[inline] is removed)

Adding an otherwise-unused pub function in the crate containing the derived implementation, which invokes Debug::fmt on &BigEnum, causes the duplicate code to disappear.

The same behavior occurs for &mut BigEnum.

This is especially problematic on embedded platforms where code size can be critical.

Reproducer

bloat-repro.zip

The crate structure is:

common
└── BigEnum with #[derive(Debug)]
pkg1
└── fn1() calls Debug::fmt(&BigEnum, ...)
pkg2
└── fn2() calls Debug::fmt(&BigEnum, ...)
main
├── calls pkg1::fn1() and pkg2::fn2()
└── calls Debug::fmt(&mut BigEnum, ...)

The essential part of common is:

#[derive(Debug)]
pub enum BigEnum {
    // The real reproducer contains enough variants/data
    // to make the generated Debug implementation substantial.
    A(u64),
    B(u64),
    C(u64),
}

pkg1:

use common::BigEnum;

pub fn fn1(be: &BigEnum) {
    println!("fn1: {be:?}");
}

pkg2 is essentially the same.

main calls both functions, as well as Debug::fmt on a &mut BigEnum.

The release profile used is:

[profile.release]
opt-level = "s"
lto = "fat"
codegen-units = 1

Observed behavior

The final executable contains two copies of <&common::BigEnum as core::fmt::Debug>::fmt.

For example (here I also printed an instance of &mut BigEnum in main):

$ readelf -sCW target/release/bloat-repro | sed -n '3p;/BigEnum/p'
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    15: 000000000000f160   911 FUNC    LOCAL  DEFAULT   15 <&mut common::BigEnum as core::fmt::Debug>::fmt
    17: 0000000000017197   911 FUNC    LOCAL  DEFAULT   15 <&common::BigEnum as core::fmt::Debug>::fmt
    18: 0000000000017526   911 FUNC    LOCAL  DEFAULT   15 <&common::BigEnum as core::fmt::Debug>::fmt

The two copies have the same demangled name and size. The mangled names show which package instantiated the functions:

$ readelf -sW target/release/bloat-repro | sed -n '3p;/BigEnum/p'
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    15: 000000000000f160   911 FUNC    LOCAL  DEFAULT   15 _RNvXs1h_NtCseLQLWk4tsmL_4core3fmtQNtCsBmZ7eE4LGl_6common7BigEnumNtB6_5Debug3fmtCs9VC4fGafmgJ_11bloat_repro
    17: 0000000000017197   911 FUNC    LOCAL  DEFAULT   15 _RNvXs1g_NtCseLQLWk4tsmL_4core3fmtRNtCsBmZ7eE4LGl_6common7BigEnumNtB6_5Debug3fmtCsgQoRw49f8cK_4pkg1
    18: 0000000000017526   911 FUNC    LOCAL  DEFAULT   15 _RNvXs1g_NtCseLQLWk4tsmL_4core3fmtRNtCsBmZ7eE4LGl_6common7BigEnumNtB6_5Debug3fmtCs91MpI6IeMrR_4pkg2
The use of #[derive(Debug)] appears to be significant.

Replacing the implementation with a handwritten one (e.g. using rust-analyzer's "Convert to manual ..." code action) appears to make the problem disappear. I did try marking the manual implementation as #[inline] (which is what I believe the derive version has) but so far I have not found a reproducer without #[derive(Debug)]. However, using the exact output from -Zunpretty=expanded I did manage to reproduce the issue.

Adding a use in the defining crate fixes the issue

If I add an otherwise-unused pub function to common which invokes Debug::fmt on &BigEnum:

impl BigEnum {
    pub fn test(&self) {
        println!("test: {self:?}");
    }
}

the duplicate <&BigEnum as Debug>::fmt functions disappear from the final executable. The function does not need to be called from main. <&mut BigEnum::Debug>::fmt copies remain, unless a call to it is added inside common as well.

Expected behavior

I would expect the final executable to contain only one copy of the relevant concrete Debug implementation when using fat LTO, regardless of whether the first use of the blanket reference implementation occurs in one downstream crate or multiple downstream crates.

In particular:

  • using #[derive(Debug)] should not cause duplicate copies of the generated implementation;
  • adding an unused function to the defining crate should not change the amount of generated code;
  • a handwritten Debug implementation and an equivalent derived implementation should not differ in this way.

Toolchain

Both stable and nightly versions reproduce the issue:

$ cargo -V
cargo 1.98.1 (797e8a9bc 2026-08-05)

$ rustc -Vv
rustc 1.98.1 (48a229cea 2026-09-01)
binary: rustc
commit-hash: 48a229ceaefd4985c50990b14116b6d856af0985
commit-date: 2026-09-01
host: x86_64-unknown-linux-gnu
release: 1.98.1
LLVM version: 22.1.8

$ cargo +nightly -V
cargo 1.100.0-nightly (3c0b53475 2026-09-04)

$ rustc +nightly -Vv
rustc 1.100.0-nightly (4aa1fbcf4 2026-09-08)
binary: rustc
commit-hash: 4aa1fbcf467cf38ce58abfa8eb9213a789c5381c
commit-date: 2026-09-08
host: x86_64-unknown-linux-gnu
release: 1.100.0-nightly
LLVM version: 23.1.1

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by building the attached bloat-repro with the shown release profile and inspect the final executable using readelf -sCW for the BigEnum Debug symbols. Compare the derived and handwritten implementations, and the common/pkg1/pkg2/main crate arrangements. Done means fat LTO emits only one copy of each relevant concrete Debug implementation without requiring an unused call in common.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.