Skip to content

DebugStruct::field_with and friends are code size footguns #149745

@hanna-kruppe

Description

@hanna-kruppe

I tried this code (artificial, for the sake of a self-contained example):

#![feature(debug_closure_helpers)]
use core::fmt::{self, Debug};

pub struct Point {
    pub x: u32,
    pub y: u32,
}

impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Point")
            .field_with("x", |f| self.x.fmt(f))
            .field_with("y", |f| self.y.fmt(f))
            .finish()
    }
}

I expected to see this happen: generating roughly comparable amounts of code to the more conventional approach with DebugStruct::field.

Instead, this happened: every use of field_with with a distinct closure monomorphizes all of field_with's code, adding a hundred lines of LLVM IR per call site. For the simple example above that only calls field_with twice, cargo-llvm-lines counts 578 lines, while the version using .field("x", &self.x).field("y", &self.y) generates 42 lines.

A real-world example where I've run into is the impl Debug for raw pointers. Here, the use of field_with causes a lot of code to be generated per pointee type. For example, consider this program:

use core::ptr::null;
use core::fmt::Debug;

type Pointers = (*mut u8, *mut u16, *mut u32, *mut u64);

pub fn force_codegen() -> Box<dyn Debug> {
    Box::new(Pointers::default())
}
cargo llvm-lines output
  Lines               Copies            Function name
  -----               ------            -------------
  1591                38                (TOTAL)
   868 (54.6%, 54.6%)  4 (10.5%, 10.5%) core::fmt::builders::DebugStruct::field_with::{{closure}}
   180 (11.3%, 65.9%)  4 (10.5%, 21.1%) core::fmt::builders::DebugStruct::field_with
   120 (7.5%, 73.4%)   4 (10.5%, 31.6%) <*const T as core::fmt::Pointer>::fmt
   103 (6.5%, 79.9%)   1 (2.6%, 34.2%)  alloc::alloc::Global::alloc_impl
    50 (3.1%, 83.0%)   1 (2.6%, 36.8%)  core::tuple::::default
    40 (2.5%, 85.5%)   4 (10.5%, 47.4%) <*const T as core::fmt::Pointer>::fmt::{{closure}}
    39 (2.5%, 88.0%)   1 (2.6%, 50.0%)  alloc::alloc::exchange_malloc
    37 (2.3%, 90.3%)   1 (2.6%, 52.6%)  core::alloc::layout::Layout::from_size_align_unchecked::precondition_check
    36 (2.3%, 92.6%)   4 (10.5%, 63.2%) <*mut T as core::fmt::Debug>::fmt
    33 (2.1%, 94.7%)   1 (2.6%, 65.8%)  core::ptr::non_null::NonNull::new_unchecked::precondition_check
    28 (1.8%, 96.4%)   4 (10.5%, 76.3%) <&T as core::fmt::Debug>::fmt
    23 (1.4%, 97.9%)   1 (2.6%, 78.9%)  <(W,V,U,T) as core::fmt::Debug>::fmt
    17 (1.1%, 98.9%)   1 (2.6%, 81.6%)  alloc::boxed::Box::new
     6 (0.4%, 99.3%)   1 (2.6%, 84.2%)  <() as core::fmt::Debug>::fmt
     6 (0.4%, 99.7%)   1 (2.6%, 86.8%)  test_field_with::force_codegen
     4 (0.3%, 99.9%)   4 (10.5%, 97.4%) core::ptr::mut_ptr::::default
     1 (0.1%,100.0%)   1 (2.6%,100.0%)  <() as core::unit::IsUnit>::is_unit

Note:

  1. This is initial LLVM IR builds. LLVM easily optimizes into jumping to the fmt_pointer_inner function for Sized pointees. But it's still wasting compile time to generate all that LLVM IR, and for unsized pointee types (e.g., *mut [T]) this will probably impact binary size even with optimizations.
  2. This specific impl can be fixed to avoid field_with (and that may be useful as a minor optimization even if field_with improves), but it's a nice example of how easy it is to run into the code size footgun of the current way these helpers are implemented.

Meta

rustc --version --verbose:

rustc 1.93.0-nightly (01867557c 2025-11-12)
binary: rustc
commit-hash: 01867557cd7dbe256a031a7b8e28d05daecd75ab
commit-date: 2025-11-12
host: x86_64-unknown-linux-gnu
release: 1.93.0-nightly
LLVM version: 21.1.5

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.I-heavyIssue: Problems and improvements with respect to binary size of generated code.T-libsRelevant to the library team, which will review and decide on the PR/issue.needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions