refactor: JSON message with less allocations#16130
Conversation
This was found during experimenting `-Zbuild-analysis` with ndjson.
From me tracing the code with `cargo expand`, basically there shouldn't
have any significant performance difference between `serde(flatten)`
and inlining all the fields. Here the differences between them
* flatten one calls `Serialize::serialize_map` without fields size
hint so cannot pre-allocate Vec with `Vec::with_capacity`,
whereas inline case calls `Serialize::serialize_struct` with a
known length of fields.
* flatten would end up calling `Serializer::serialize_map`
and line calls `Serializer::serialize_struct`. And in serde_json
serializer `serialize_struct` actually call `serailze_map`.
So no difference on serializer side.
* There might be some function calls not inlined I like
`FlatMapSerializer` but I doubt it is costly than allocation.
Here is the `cargo-expand`'d result:
```rust
#[derive(Serialize)]
pub struct Foo<D: Serialize> {
id: u8,
#[serde(flatten)]
data: D,
}
#[derive(Serialize)]
struct Bar {
a: bool,
}
// Expand to
extern crate serde as _serde;
impl<D: Serialize> _serde::Serialize for Foo<D>
where
D: _serde::Serialize,
{
fn serialize<__S>(
&self,
__serializer: __S,
) -> _serde::__private228::Result<__S::Ok, __S::Error>
where
__S: _serde::Serializer,
{
let mut __serde_state = _serde::Serializer::serialize_map(
__serializer,
_serde::__private228::None,
)?;
_serde::ser::SerializeMap::serialize_entry(
&mut __serde_state,
"id",
&self.id,
)?;
_serde::Serialize::serialize(
&&self.data,
_serde::__private228::ser::FlatMapSerializer(&mut __serde_state),
)?;
_serde::ser::SerializeMap::end(__serde_state)
}
}
impl _serde::Serialize for Bar {
fn serialize<__S>(
&self,
__serializer: __S,
) -> _serde::__private228::Result<__S::Ok, __S::Error>
where
__S: _serde::Serializer,
{
let mut __serde_state = _serde::Serializer::serialize_struct(
__serializer,
"Bar",
false as usize + 1,
)?;
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"a",
&self.a,
)?;
_serde::ser::SerializeStruct::end(__serde_state)
}
}
```
```rust
#[derive(Serialize)]
pub struct Foo<D: Serialize> {
id: u8,
a: bool,
}
// Expand to
impl<D: Serialize> _serde::Serialize for Foo<D> {
fn serialize<__S>(
&self,
__serializer: __S,
) -> _serde::__private228::Result<__S::Ok, __S::Error>
where
__S: _serde::Serializer,
{
let mut __serde_state = _serde::Serializer::serialize_struct(
__serializer,
"Foo",
false as usize + 1 + 1,
)?;
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"id",
&self.id,
)?;
_serde::ser::SerializeStruct::serialize_field(
&mut __serde_state,
"a",
&self.a,
)?;
_serde::ser::SerializeStruct::end(__serde_state)
}
}
```
|
Just used Claude Haiku 4.5 to generate a criterioin benchamrk script for me. The benchmark was run under Here is the result:
BTW, it is interesting that I can directly pull in local path dependencies in
|
Deserialize is where differences are noticed but we're not really setup for that so it should be fine |
I'm surprised we noticed that big of a difference for Since this simplifies the code, I'm good with it. I do like having In case there is something unexpected, like key ordering, I looked up when this code was added (#6081) and it doesn't seem like we're losing anything with this change. |
Update cargo submodule 7 commits in 367fd9f213750cd40317803dd0a5a3ce3f0c676d..344c4567c634a25837e3c3476aac08af84cf9203 2025-10-15 15:01:32 +0000 to 2025-10-21 21:29:43 +0000 - refactor: Centralize CONTEXT style (rust-lang/cargo#16135) - chore(triagebot): `A-json-output` for machine_message.rs (rust-lang/cargo#16133) - refactor: JSON message with less allocations (rust-lang/cargo#16130) - More warning conversions (rust-lang/cargo#16126) - fix(check): Fix suggested command for bin package (rust-lang/cargo#16127) - fix(script): Remove name sanitiztion outside what is strictly required (rust-lang/cargo#16120) - refactor: Centralize some more styling (rust-lang/cargo#16124) r? ghost
What does this PR try to resolve?
This was found during experimenting
-Zbuild-analysiswith ndjson.From me tracing the code with
cargo expand, basically there shouldn't have any significant performance difference betweenserde(flatten)and inlining all the fields. Here the differences between themSerialize::serialize_mapwithout fields size hint so cannot pre-allocate Vec withVec::with_capacity, whereas inline case callsSerialize::serialize_structwith a known length of fields.Serializer::serialize_mapand line callsSerializer::serialize_struct. And in serde_json serializerserialize_structactually callserailze_map. So no difference on serializer side.FlatMapSerializerbut I doubt it is costly than allocation.Here is the
cargo-expand'd result:How to test and review this PR?
CI passing.
One change is that
reasonwill no longer be the first field. We could activate thepreserve_orderfeature inserde_jsonif we want, though that may get more perf loess than the gain.See the benchmark result in #16130 (comment)