Cross-crate duplicate codegen for `#[derive(Debug)]` via `Debug for &T`
Nobody has claimed this yet.
- 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
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 . However, using the exact output from #[derive(Debug)]-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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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