This is almost always what we want: #[inline] is just a hint,
and typical derived Debug, Clone, etc. implementations benefit
from being inlined (since they're often trivial).
But not always! Imagine an error hierarchy like this[1]:
#[derive(Debug)]
struct ErrorA {
lots: String,
of: String,
chunky: String,
fields: String,
within: String,
this: String,
r#type: String,
}
#[derive(Debug)]
struct ErrorB {
inner: ErrorA,
}
#[derive(Debug)]
struct ErrorC {
inner: ErrorB,
}
#[derive(Debug)]
enum Errors {
A(ErrorA),
B(ErrorB),
C(ErrorC),
}
produces:
struct ErrorA {
lots: String,
of: String,
chunky: String,
fields: String,
within: String,
this: String,
r#type: String,
}
#[automatically_derived]
impl ::core::fmt::Debug for ErrorA {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ =
&["lots", "of", "chunky", "fields", "within", "this", "type"];
let values: &[&dyn ::core::fmt::Debug] =
&[&self.lots, &self.of, &self.chunky, &self.fields, &self.within,
&self.this, &&self.r#type];
::core::fmt::Formatter::debug_struct_fields_finish(f, "ErrorA", names,
values)
}
}
#[automatically_derived]
impl ::core::default::Default for ErrorA {
#[inline]
fn default() -> Self {
Self {
lots: ::core::default::Default::default(),
of: ::core::default::Default::default(),
chunky: ::core::default::Default::default(),
fields: ::core::default::Default::default(),
within: ::core::default::Default::default(),
this: ::core::default::Default::default(),
r#type: ::core::default::Default::default(),
}
}
}
struct ErrorB {
inner: ErrorA,
}
#[automatically_derived]
impl ::core::fmt::Debug for ErrorB {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f, "ErrorB",
"inner", &&self.inner)
}
}
struct ErrorC {
inner: ErrorB,
}
#[automatically_derived]
impl ::core::fmt::Debug for ErrorC {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f, "ErrorC",
"inner", &&self.inner)
}
}
enum Errors { A(ErrorA), B(ErrorB), C(ErrorC), }
#[automatically_derived]
impl ::core::fmt::Debug for Errors {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
Self::A(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "A",
&__self_0),
Self::B(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "B",
&__self_0),
Self::C(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "C",
&__self_0),
}
}
}
That's a lot of code that can get inlined for each invocation of the Debug
implementation of Errors, which can occur repeatedly in e.g. debug or trace logging.
In fact, it's so much code that it can turn out to be a non-trivial amount of a Rust
binary's total size: we found that we could shrink uv's binary size by approximately
160KB by preventing[2] rustc from inlining a given Debug implementation.
This was surprising to me on two levels: the size cost added up fast, and rustc
(seemingly) did not apply a limit to the size or number of times a Debug implementation
was inlined. I suspect this is the right decision in many programs, however!
-
This hierarchy is drastically simplified: real world Rust applications often have
deeply nested error enumerations with nontrivial numbers of fields. [↩]
-
We did this by adding our own-proc macro that behaves like derive(Debug), but
with #[inline(never)]. [↩]