-
Notifications
You must be signed in to change notification settings - Fork 2.7k
refactor: JSON message with less allocations #16130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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)