Skip to content

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

Description

@baltazar-arm

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

Activity

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

    C-bugCategory: This is a bug.needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions