Content:缓冲的代价
主线位置:缓冲主线的中段。untagged、内部标签、
flatten生成的代码在「还不知道该交给哪个Visitor」时,先用ContentVisitor把输入读成一棵Content树(读入),再让ContentDeserializer或ContentRefDeserializer假装成格式,对这棵树把反序列化主线再跑一遍(重放)。本页讲这棵树、读入和重放这两步,以及中间丢掉了什么、多花了多少。三种写法各自怎么调用它们,见 内部标签与相邻标签、untagged 和 flatten。
flowchart TB gen["生成的代码"] --> seed["ContentVisitor 作 seed"] seed --> hook["__deserialize_content_v1"] hook -->|默认| anyde["格式的 deserialize_any"] hook -->|"格式自己覆盖"| own["格式自定"] anyde --> tree["Content 树"] own --> tree tree --> owned["ContentDeserializer"] tree --> byref["ContentRefDeserializer"] owned --> replay["T::deserialize 再跑一遍"] byref --> replay replay -->|"deserialize_u8 等提示"| cb["按变体回调"]
本页的实验都在 M2 Max、macOS、rustc 1.99.0 上跑,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"、bincode = "=1.3.3"、postcard = "1"(1.1.3)、serde_yaml = "=0.9.34";arbitrary_precision 的实验单独放在一个打开了这个 feature 的 crate 里(cargo 会合并 feature,不能和别的实验混在一起)。
Content 的定义:为什么在 serde_core
Content<'de> 是整个文件 serde_core/src/private/content.rs 的全部内容:
// Used from generated code to buffer the contents of the Deserializer when
// deserializing untagged enums and internally tagged enums.
//
// Not public API. Use serde-value instead.
//
// Obsoleted by format-specific buffer types (https://github.com/serde-rs/serde/pull/2912).
#[doc(hidden)]
pub enum Content<'de> {
Bool(bool),
U8(u8),
U16(u16),
U32(u32),
U64(u64),
I8(i8),
I16(i16),
I32(i32),
I64(i64),
F32(f32),
F64(f64),
Char(char),
String(String),
Str(&'de str),
ByteBuf(Vec<u8>),
Bytes(&'de [u8]),
None,
Some(Box<Content<'de>>),
Unit,
Newtype(Box<Content<'de>>),
Seq(Vec<Content<'de>>),
Map(Vec<(Content<'de>, Content<'de>)>),
}
22 个变体,对照数据模型的 29 种类型,少的和合并的是:
| 数据模型 | Content 里 | 后果 |
|---|---|---|
i128、u128 | 没有 | 读不进来,也放不出去(本页后面实验) |
| unit struct | Unit | 名字丢了 |
| newtype struct | Newtype | 名字丢了;JSON 根本不产生它,见下文 deserialize_newtype_struct |
| tuple、tuple struct、seq | 都是 Seq | 长度和名字丢了 |
| map、struct | 都是 Map | 名字和字段表丢了 |
| 四种 enum 变体 | 没有 | ContentVisitor::visit_enum 直接报错 |
str | Str(&'de str) 和 String(String) | 能借的借,借不成的拥有,见 生命周期 'de 与借用 |
bytes | Bytes(&'de [u8]) 和 ByteBuf(Vec<u8>) | 同上 |
Map 是 Vec 而不是 HashMap:键的顺序保留下来,重复键也保留,键也可以不是字符串。
定义放在 serde_core,读写它的代码却都在 serde/src/private/de.rs。原因在 Deserializer trait 上一个隐藏方法的签名里(下一节):trait 定义在 serde_core,签名里出现的类型也只能来自 serde_core,见源码地图。除了这个签名,serde_core 里没有任何代码用到 Content;它甚至没有 Deserialize impl(Deserialize 和 Content 都在 serde_core,按孤儿规则 serde 写不了这个 impl),1.0.222(#2976,「Move Content's Deserialize impl from serde_core to serde」)之后,「把输入读成 Content」这件事由 serde 里的 ContentVisitor 作为 DeserializeSeed 来做。
ContentVisitor:读进来
ContentVisitor 有两个身份。作为 DeserializeSeed,它只做一件事:
impl<'de> DeserializeSeed<'de> for ContentVisitor<'de> {
type Value = Content<'de>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.__deserialize_content_v1(self)
}
}
作为 Visitor,它把每个 visit_* 原样变成一个变体:visit_u8(v) 得到 Content::U8(v),visit_borrowed_str(v) 得到 Content::Str(v),visit_str(v) 得到 Content::String(v.into())(拷贝一份),visit_string(v) 直接收下。复合类型递归,visit_map:
fn visit_map<V>(self, mut visitor: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut vec =
Vec::<(Content, Content)>::with_capacity(
size_hint::cautious::<(Content, Content)>(visitor.size_hint()),
);
while let Some(kv) =
tri!(visitor.next_entry_seed(ContentVisitor::new(), ContentVisitor::new()))
{
vec.push(kv);
}
Ok(Content::Map(vec))
}
每个键、每个值都用一个新的 ContentVisitor 作 seed 去读,所以树里的每个节点都各经过一次那个隐藏方法。size_hint::cautious 防止恶意长度一次分配太多,见标准库类型的实现。
有三个 visit_* 读不进 Content:visit_i128、visit_u128 没有实现,落到默认实现报 invalid_type(这两个默认不转发,见 Deserialize、Deserializer 与 Visitor);visit_enum 是写出来的,但只返回一个错误:
fn visit_enum<V>(self, _visitor: V) -> Result<Self::Value, V::Error>
where
V: EnumAccess<'de>,
{
Err(de::Error::custom(
"untagged and internally tagged enums do not support enum input",
))
}
JSON 的 deserialize_any 从不调 visit_enum,碰不到这一条。YAML 的 !Tag value 写法会:serde_yaml 的 deserialize_any 遇到 !A 1 这样带标签的值,调的是 visit_enum。src/bin/yaml_enum.rs:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
enum E {
A(u8),
}
#[derive(Deserialize, Debug)]
struct Inner {
e: E,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Tagged {
V(Inner),
}
fn main() {
let yaml = "t: V\ne: !A 1\n";
println!("plain {:?}", serde_yaml::from_str::<Inner>(yaml).map_err(|e| e.to_string()));
println!("tagged {:?}", serde_yaml::from_str::<Tagged>(yaml).map_err(|e| e.to_string()));
}
plain Ok(Inner { e: A(1) })
tagged Err("e: untagged and internally tagged enums do not support enum input at line 2 column 4")
同一段 YAML,Inner 直接读没问题;包进内部标签的变体之后,e 的值要先进 Content,而 Content 里没有 enum。
__deserialize_content_v1:留给格式的钩子
ContentVisitor 作 seed 时调的是 Deserializer 上的 __deserialize_content_v1:
// Not public API.
#[cfg(all(not(no_serde_derive), any(feature = "std", feature = "alloc")))]
#[doc(hidden)]
fn __deserialize_content_v1<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de, Value = crate::private::Content<'de>>,
{
self.deserialize_any(visitor)
}
默认就是 deserialize_any:格式不知道调用方想要什么,只能照输入里实际是什么回调。no_serde_derive 是 serde_core/build.rs 在 rustc 低于 1.71(serde_derive 的最低版本)时设的 cfg,那时没有 derive,也就用不到它。
谁覆盖了它?在 serde 1.0.229 和 serde_json 1.0.154 的源码里搜 __deserialize_content,一共四处:上面的默认实现、ContentVisitor 的调用,和 private/de.rs 里两个重放用的 Deserializer。serde_json 没有覆盖,本机 cargo 缓存里的 bincode、postcard、serde_yaml、toml 也都没有。两个覆盖都很短,ContentDeserializer 的覆盖:
fn __deserialize_content_v1<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de, Value = Content<'de>>,
{
let _ = visitor;
Ok(self.content)
}
ContentRefDeserializer 的覆盖是 Ok(content_clone(self.content))。意思是:如果「格式」本身已经是一棵 Content(缓冲套着缓冲,比如内部标签的变体里又是一个 untagged enum),里层要缓冲时直接把这棵树交出去,不用再 deserialize_any 一遍、按节点重新建一棵。
下面给 02-core 里用过的打印调用的 LogDe 加上这个方法(只打印一行,然后转给被包的格式),看 serde_json 收到的是什么。src/log_de.rs 里新加的部分:
// The hidden hook: log it, then let the wrapped format decide (serde_json keeps the default).
fn __deserialize_content_v1<V>(self, visitor: V) -> Result<V::Value, D::Error>
where
V: Visitor<'de, Value = serde::__private229::de::Content<'de>>,
{
log(self.depth, "__deserialize_content_v1");
self.inner.__deserialize_content_v1(LogV { inner: visitor, depth: self.depth + 1 })
}
src/bin/hook.rs:
use exp_serde_content::log_de::LogDe;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Inner {
x: u8,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Untagged {
V(Inner),
}
fn main() {
let input = r#"{"x":7}"#;
println!("Inner:");
let mut de = serde_json::Deserializer::from_str(input);
println!("=> {:?}", Inner::deserialize(LogDe::new(&mut de)).unwrap());
println!("Untagged:");
let mut de = serde_json::Deserializer::from_str(input);
println!("=> {:?}", Untagged::deserialize(LogDe::new(&mut de)).unwrap());
}
Inner:
deserialize_struct("Inner", ["x"])
visit_map
next_key_seed
deserialize_identifier
visit_borrowed_str("x")
-> Some
next_value_seed
deserialize_u8
visit_u64(7)
next_key_seed
-> None
=> Inner { x: 7 }
Untagged:
__deserialize_content_v1
visit_map
size_hint() = None
next_entry_seed
__deserialize_content_v1
visit_borrowed_str("x")
__deserialize_content_v1
visit_u64(7)
-> Some
next_entry_seed
-> None
=> V(Inner { x: 7 })
- 直接读
Inner,serde_json 收到deserialize_struct、deserialize_identifier、deserialize_u8三个提示。 - 读
Untagged,serde_json 只收到__deserialize_content_v1,键和值各一次,默认实现在内部转成deserialize_any(它没被LogDe包住,所以没打出来)。deserialize_u8这个提示根本没到 serde_json:它在重放时发给了ContentRefDeserializer。 - 键值是成对读的(
next_entry_seed),size_hint()是None:JSON 对象事先不知道有几个键。
__deserialize_content,拆 crate 时丢过一次#2146(「Improve performance using nightly features」)报告:在优化一个项目的反序列化性能时发现,Content 经常是从一个 ContentDeserializer 再读出来的(「__deserializer is serde::__private::de::ContentDeserailizer in many cases」);用 nightly 的 specialization 让这种情况直接 Ok(deserializer.content) 之后,基准从「1,226,131 ns/iter」降到「621,480 ns/iter」。#2148(「Optimize deserialization of recursive buffered types」,首次发布于 1.0.133)在稳定版上做到了同样的事:给 Deserializer 加一个隐藏方法 __deserialize_content,默认 deserialize_any,ContentDeserializer 覆盖成直接返回自己的 content,ContentRefDeserializer 覆盖成 clone。1.0.133 的发布说明是:「Optimize deserialization of data structures that contain recursive use of flatten fields or tag or untagged enums (#2148)」。
1.0.220 把 trait 拆进 serde_core 的提交(#2608)删掉了这个方法,ContentVisitor 改回直接调 deserialize_any;当时 Content 还定义在 serde 的 private/de.rs 里。同一个版本里 #2968(「Restore __deserialize_content optimization」)把 Content 的定义搬进 serde_core/src/private,方法以 __deserialize_content_v1 的名字加回来;本页开头那句「Obsoleted by format-specific buffer types」的注释也是这次加上的。两次改动都在 1.0.220 里,没有一个发布版缺这个优化。
这个钩子在本页的分配实验里还会出现:内部标签套 untagged,第二层缓冲不多分配一次。
重放:两个 ContentDeserializer
读进来的树要交给真正的类型,T::deserialize 需要一个 Deserializer。private/de.rs 里有两个:
ContentDeserializer | ContentRefDeserializer | |
|---|---|---|
| 字段 | content: Content<'de> | content: &'a Content<'de>,Copy |
| 注释 | 「Used when deserializing an internally tagged enum because the content will be used exactly once.」 | 「Used when deserializing an untagged enum because the content may need to be used more than once.」 |
Content::String 交出去 | visit_string(v),移交所有权 | visit_str(v),对方要 String 就得再拷贝一次 |
| 谁在用 | 内部标签;flatten 进 struct 时被认领的键值 | untagged(每个变体试一次);flatten 进 map 时 |
两者的 Error 都是类型参数 E,生成的代码填的是格式的错误类型(ContentRefDeserializer::<__D::Error>::new(&__content)),所以重放时产生的错误还是 serde_json::Error,只是没有了位置(后面讲)。
提示怎么映射到 Content
重放时 T::deserialize 照常发提示,ContentDeserializer 按手里的变体回调。整数的提示都进同一个函数,deserialize_integer:
fn deserialize_integer<V>(self, visitor: V) -> Result<V::Value, E>
where
V: Visitor<'de>,
{
match self.content {
Content::U8(v) => visitor.visit_u8(v),
Content::U16(v) => visitor.visit_u16(v),
Content::U32(v) => visitor.visit_u32(v),
Content::U64(v) => visitor.visit_u64(v),
Content::I8(v) => visitor.visit_i8(v),
Content::I16(v) => visitor.visit_i16(v),
Content::I32(v) => visitor.visit_i32(v),
Content::I64(v) => visitor.visit_i64(v),
_ => Err(self.invalid_type(&visitor)),
}
}
deserialize_i8 到 deserialize_u64 八个方法都是 self.deserialize_integer(visitor):提示是 u8,内容是 Content::U64(7),就调 visit_u64(7),由 u8 的 Visitor 自己做范围检查(见标准库类型的实现)。提示在这里只起一个作用:内容不是整数时报错。deserialize_float 多接受八种整数变体,所以 f32 字段能读 7;反过来 deserialize_integer 不接受 F64,u8 字段读 7.0 报类型错。
另外两处有讲究:
deserialize_option(两个Deserializer写法相同,注释在ContentRefDeserializer那份里):None调visit_none,Some拆一层,Unit调visit_unit,其余一律visit_some(self)。注释说这是为了 JSON 这类「do not encode an indication whether a value is optional」的格式:JSON 的deserialize_any从不调visit_some,7进来就是Content::U64(7),Option<u8>字段得能把它读成Some(7)。null进来是Content::Unit(serde_json 的deserialize_any对null调visit_unit),Option的Visitor把visit_unit当None。deserialize_newtype_struct:同样的道理,不是Content::Newtype的内容也当成 newtype 的内层,visitor.visit_newtype_struct(self)。
deserialize_u128 和 deserialize_i128 两个都没写,用的是 trait 的默认实现 deserialize_u128:Err(Error::custom("u128 is not supported"))。
实验:同一个字段,三条路
用一组「探针」类型看提示和回调的对应:每个探针的 Deserialize 只发一个固定的提示,Visitor 把收到的 visit_* 和值记成字符串;出错也记下来,探针本身不失败,这样 untagged 不会因为某个字段出错而整体失败。src/lib.rs(删掉了重复的 visit_*):
pub struct Log;
impl<'de> Visitor<'de> for Log {
type Value = String;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("anything")
}
// visit_bool ... visit_byte_buf: 都是 Ok(format!("{}({:?})", 方法名, v))
fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<String, D::Error> {
Ok(format!("visit_some({})", d.deserialize_any(Log)?))
}
// visit_unit、visit_none、visit_newtype_struct、visit_seq、visit_map 同理
}
macro_rules! probe {
($($ty:ident => $method:ident),*) => {
$(
pub struct $ty(pub String);
// ... impl Debug:打印 self.0
impl<'de> de::Deserialize<'de> for $ty {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Ok($ty(match d.$method(Log) {
Ok(s) => s,
Err(e) => format!("Err({e})"),
}))
}
}
)*
};
}
probe! {
PU8 => deserialize_u8,
PU128 => deserialize_u128,
PStr => deserialize_str,
PString => deserialize_string,
PBytes => deserialize_bytes,
POption => deserialize_option,
PBool => deserialize_bool
// ...
}
src/bin/hints.rs:同一个 Fields 直接读、作为内部标签的变体读、作为 untagged 的变体读:
#[derive(Deserialize, Debug)]
struct Fields {
a: PU8,
b: PU128,
big: PU128,
s: PStr,
esc: PStr,
owned: PString,
bytes: PBytes,
opt: POption,
flag: PBool,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Tagged {
V(Fields),
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Untagged {
V(Fields),
}
const INPUT: &str = r#"{"t":"V","a":7,"b":7,"big":18446744073709551616,
"s":"hi","esc":"a\nb","owned":"hi","bytes":"hi","opt":7,"flag":"true"}"#;
// show() 逐行打印九个字段
fn main() {
show("direct (serde_json::Deserializer)", &serde_json::from_str::<Fields>(INPUT).unwrap());
let Tagged::V(f) = serde_json::from_str::<Tagged>(INPUT).unwrap();
show("tag = \"t\" (ContentDeserializer)", &f);
let Untagged::V(f) = serde_json::from_str::<Untagged>(INPUT).unwrap();
show("untagged (ContentRefDeserializer)", &f);
}
direct (serde_json::Deserializer)
a deserialize_u8 -> visit_u64(7)
b deserialize_u128 -> visit_u128(7)
big deserialize_u128 -> visit_u128(18446744073709551616)
s deserialize_str -> visit_borrowed_str("hi")
esc deserialize_str -> visit_str("a\nb")
owned deserialize_string -> visit_borrowed_str("hi")
bytes deserialize_bytes -> visit_borrowed_bytes([104, 105])
opt deserialize_option -> visit_some(visit_u64(7))
flag deserialize_bool -> Err(invalid type: string "true", expected anything at line 2 column 71)
tag = "t" (ContentDeserializer)
a deserialize_u8 -> visit_u64(7)
b deserialize_u128 -> Err(u128 is not supported)
big deserialize_u128 -> Err(u128 is not supported)
s deserialize_str -> visit_borrowed_str("hi")
esc deserialize_str -> visit_string("a\nb")
owned deserialize_string -> visit_borrowed_str("hi")
bytes deserialize_bytes -> visit_borrowed_str("hi")
opt deserialize_option -> visit_some(visit_u64(7))
flag deserialize_bool -> Err(invalid type: string "true", expected anything)
untagged (ContentRefDeserializer)
a deserialize_u8 -> visit_u64(7)
b deserialize_u128 -> Err(u128 is not supported)
big deserialize_u128 -> Err(u128 is not supported)
s deserialize_str -> visit_borrowed_str("hi")
esc deserialize_str -> visit_str("a\nb")
owned deserialize_string -> visit_borrowed_str("hi")
bytes deserialize_bytes -> visit_borrowed_str("hi")
opt deserialize_option -> visit_some(visit_u64(7))
flag deserialize_bool -> Err(invalid type: string "true", expected anything)
逐行看:
a:三条路一样。serde_json 对deserialize_u8本来也是解析出一个数再visit_u64,缓冲没有改变什么。b、big:直接读时 serde_json 的deserialize_u128自己解析数字,连超出u64的18446744073709551616也能读;缓冲之后两个都是u128 is not supported,连7都不行。内容在Content里是什么(U64(7)、F64)无关紧要,ContentDeserializer根本没有实现这个方法。s:没有转义的字符串在Content里是Str,重放时还能借。esc:有转义,serde_json 只能visit_str,进Content时拷贝成String。重放时两条缓冲路分开了:拿所有权的移交(visit_string),借用的只能再给一个&str(visit_str)。bytes:直接读时 serde_json 按提示把字符串当字节给出(visit_borrowed_bytes);Content里存的是Str,重放时deserialize_bytes交出去的是visit_borrowed_str。要字节的一方得自己也接受字符串。opt:三条路一样,靠的是上面deserialize_option的「其余一律visit_some」。flag:报错一样,但直接读的那条带着at line 2 column 71,缓冲的两条没有位置。
丢掉了什么
把上面的探针换成真实的类型,看用户会碰到什么。src/bin/losses.rs:
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned;
#[derive(Serialize, Deserialize, Debug)]
struct Big {
n: u128,
}
#[derive(Serialize, Deserialize, Debug)]
struct Keys {
m: HashMap<u32, bool>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Small {
x: u8,
}
// The same struct three ways: as is, as an internally tagged variant, as an untagged variant.
macro_rules! wrap {
($inner:ident, $tagged:ident, $untagged:ident) => {
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "t")]
enum $tagged {
V($inner),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum $untagged {
V($inner),
}
};
}
wrap!(Big, BigTagged, BigUntagged);
wrap!(Keys, KeysTagged, KeysUntagged);
wrap!(Small, SmallTagged, SmallUntagged);
fn json<T: DeserializeOwned + std::fmt::Debug>(label: &str, s: &str) {
match serde_json::from_str::<T>(s) {
Ok(v) => println!(" {label:<9} Ok({v:?})"),
Err(e) => println!(" {label:<9} Err({e})"),
}
}
fn main() {
let big = r#"{"t":"V","n":7}"#;
println!("u128 field, input {big}");
json::<Big>("plain", big);
json::<BigTagged>("tag", big);
json::<BigUntagged>("untagged", big);
// ... Keys 用 {"t":"V","m":{"1":true}},Small 用 {"t":"V","x":300},同样三行
println!("non-self-describing formats, value SmallTagged::V(Small {{ x: 7 }})");
let v = SmallTagged::V(Small { x: 7 });
let b = bincode::serialize(&v).unwrap();
println!(" bincode bytes {b:?}");
println!(" bincode back {:?}", bincode::deserialize::<SmallTagged>(&b).map_err(|e| e.to_string()));
let p = postcard::to_allocvec(&v).unwrap();
println!(" postcard bytes {p:?}");
println!(" postcard back {:?}", postcard::from_bytes::<SmallTagged>(&p).map_err(|e| e.to_string()));
let b = bincode::serialize(&SmallUntagged::V(Small { x: 7 })).unwrap();
println!(" bincode untagged back {:?}", bincode::deserialize::<SmallUntagged>(&b).map_err(|e| e.to_string()));
}
u128 field, input {"t":"V","n":7}
plain Ok(Big { n: 7 })
tag Err(u128 is not supported)
untagged Err(data did not match any variant of untagged enum BigUntagged)
HashMap<u32, bool> field, input {"t":"V","m":{"1":true}}
plain Ok(Keys { m: {1: true} })
tag Err(invalid type: string "1", expected u32)
untagged Err(data did not match any variant of untagged enum KeysUntagged)
u8 field, input {"t":"V","x":300}
plain Err(invalid value: integer `300`, expected u8 at line 1 column 16)
tag Err(invalid value: integer `300`, expected u8)
untagged Err(data did not match any variant of untagged enum SmallUntagged)
non-self-describing formats, value SmallTagged::V(Small { x: 7 })
bincode bytes [1, 0, 0, 0, 0, 0, 0, 0, 86, 7]
bincode back Err("Bincode does not support the serde::Deserializer::deserialize_any method")
postcard bytes [1, 86, 7]
postcard back Err("This is a feature that PostCard will never implement")
bincode untagged back Err("Bincode does not support the serde::Deserializer::deserialize_any method")
下面逐条说。untagged 那一列把每个变体的失败原因都吞成了一句话,为什么见 untagged;内部标签那一列能看到真正的原因。
u128 / i128。 上一节说过,读入(ContentVisitor 没有 visit_u128)和重放(ContentDeserializer 没有 deserialize_u128)两头都不支持。JSON 这里是重放那一头先失败:7 进 Content 时是 U64(7),读入没问题,u128 字段一要就报错。
格式按提示做的特殊处理。 HashMap<u32, bool> 直接读能行,是因为 serde_json 的 MapKey 收到 deserialize_u32 提示时走 deserialize_numeric_key!,把带引号的 "1" 当数字解析。缓冲时键是按 deserialize_any 读的,在 Content 里就是 Str("1"),重放时 deserialize_u32 对着一个字符串只能报 invalid type: string "1", expected u32。这正是 #1183(「Internal buffering disrupts format-specific deserialization features」,至今 open)里的例子。flatten 进一个数字键的 map 是同一个问题,见 flatten。
报错位置。 直接读时错误带 at line 1 column 16。serde_json 在 deserialize_struct、deserialize_any 等方法返回前对出错的结果调 fix_position,给没有位置的错误补上当前读到的位置(见错误)。内部标签和 untagged 的重放发生在 deserialize_any 已经返回之后,这时 serde_json 不在调用栈上,错误一路冒出去也没人补位置。
非自描述格式。 写出没问题(bincode 的 [1, 0, 0, 0, 0, 0, 0, 0, 86, 7] 是长度 1 的字符串 "V" 加一个字节 7),读回来必须 deserialize_any,bincode 和 postcard 都不支持(postcard 的报错是它的 WontImplement 错误)。为什么这两个格式答不了「这里是什么」,见自描述格式与非自描述格式。
arbitrary_precision:数字变成了 map
serde_json 打开 arbitrary_precision 后,Number 内部存的是原始的十进制字符串。为了把这段字符串经过数据模型交给 serde_json::Value,parse_any_number 只把放得进 u64 / i64 的整数照常给出,其余(小数、超出范围的整数、-0)都变成 ParserNumber::String,ParserNumber::visit 对它调 visitor.visit_map(NumberDeserializer { .. }):一个只有一个键的 map,键是魔法名字 $serde_json::private::Number(这套约定见 Value)。f64 的 Visitor 不认 map。直接读时没事,因为 serde_json 的 deserialize_f64 经 deserialize_number 调的是 parse_integer,不经过 parse_any_number,照常 visit_f64;缓冲时 deserialize_any 交出的就是这个 map,Content 原样存成 Map。
另一个只打开了 arbitrary_precision 和 raw_value 的实验 crate,src/bin/arbitrary.rs:
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde_json::value::RawValue;
#[derive(Deserialize, Debug)]
struct Num {
n: u64,
f: f64,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum NumTagged {
V(Num),
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum NumUntagged {
V(Num),
}
#[derive(Deserialize, Debug)]
struct Val {
n: serde_json::Value,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum ValTagged {
V(Val),
}
#[derive(Deserialize, Debug)]
struct Raw {
r: Box<RawValue>,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum RawTagged {
V(Raw),
}
// json() 同上
fn main() {
let num = r#"{"t":"V","n":7,"f":1.5}"#;
println!("input {num}");
json::<Num>("plain", num);
json::<NumTagged>("tag", num);
json::<NumUntagged>("untagged", num);
let val = r#"{"t":"V","n":3.14159265358979323846264338327950288}"#;
println!("input {val}");
json::<Val>("Value plain", val);
json::<ValTagged>("Value tag", val);
let raw = r#"{"t":"V","r":[1, 2]}"#;
println!("input {raw}");
json::<Raw>("RawValue plain", raw);
json::<RawTagged>("RawValue tag", raw);
}
input {"t":"V","n":7,"f":1.5}
plain Ok(Num { n: 7, f: 1.5 })
tag Err(invalid type: map, expected f64)
untagged Err(data did not match any variant of untagged enum NumUntagged)
input {"t":"V","n":3.14159265358979323846264338327950288}
Value plain Ok(Val { n: Number(3.14159265358979323846264338327950288) })
Value tag Ok(V(Val { n: Number(3.14159265358979323846264338327950288) }))
input {"t":"V","r":[1, 2]}
RawValue plain Ok(Raw { r: RawValue([1, 2]) })
RawValue tag Err(invalid type: newtype struct, expected any valid JSON value)
n: u64读7没问题(放得进u64,照常visit_u64),f: f64读1.5报invalid type: map, expected f64。所以「打开arbitrary_precision后内部标签里的整数还能用、小数全坏」:只有一部分数字受影响,容易在测试里漏掉。Value不受影响:它的Visitor认得那个魔法键,从Content::Map重放出来照样还原成Number,精度也在。RawValue坏掉的原因不同:它按魔法名字调deserialize_newtype_struct,要求格式交出原始的 JSON 片段;缓冲之后原始文本已经没了,ContentDeserializer只能把内容当成 newtype 的内层(visit_newtype_struct(self)),RawValue的Visitor拒绝。
代价:多几次分配
缓冲的直接开销是:建一棵树(每个 Seq、Map 一个 Vec,String、ByteBuf 各一块),再遍历一次。下面用一个计数的全局分配器数一次 from_str 里的分配次数和字节数(realloc 也算一次),再在 release 下跑 20 万次取平均耗时。src/bin/cost.rs:
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use std::time::Instant;
use serde::Deserialize;
use serde::de::DeserializeOwned;
// Counts every allocation (and its bytes) made by the process.
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static BYTES: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Relaxed);
BYTES.fetch_add(layout.size(), Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Relaxed);
BYTES.fetch_add(new_size, Relaxed);
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
#[derive(Deserialize, Debug)]
struct Post {
title: String,
tags: Vec<String>,
views: u64,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Tagged {
Post(Post),
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Untagged {
Post(Post),
}
#[derive(Deserialize, Debug)]
struct Flat {
#[serde(flatten)]
post: Post,
}
// Internally tagged around untagged: buffering inside buffering.
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Outer {
Post(UntaggedInner),
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum UntaggedInner {
Post(Post),
}
// Hides the hook: every method, including __deserialize_content_v1, ends in deserialize_any.
struct NoHook<D>(D);
impl<'de, D: serde::Deserializer<'de>> serde::Deserializer<'de> for NoHook<D> {
type Error = D::Error;
fn deserialize_any<V: serde::de::Visitor<'de>>(self, v: V) -> Result<V::Value, D::Error> {
self.0.deserialize_any(v)
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum identifier ignored_any
}
}
#[derive(Debug)]
struct HiddenInner(UntaggedInner);
impl<'de> Deserialize<'de> for HiddenInner {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
UntaggedInner::deserialize(NoHook(d)).map(HiddenInner)
}
}
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum OuterNoHook {
Post(HiddenInner),
}
fn measure<T: DeserializeOwned>(label: &str, input: &str) {
let a0 = ALLOCS.load(Relaxed);
let b0 = BYTES.load(Relaxed);
let v: T = serde_json::from_str(input).unwrap();
let allocs = ALLOCS.load(Relaxed) - a0;
let bytes = BYTES.load(Relaxed) - b0;
drop(v);
const N: u32 = 200_000;
let t = Instant::now();
for _ in 0..N {
let v: T = serde_json::from_str(std::hint::black_box(input)).unwrap();
std::hint::black_box(v);
}
let ns = t.elapsed().as_nanos() / N as u128;
println!("{label:<22} allocs={allocs:<3} bytes={bytes:<5} {ns} ns");
}
fn main() {
println!("size_of::<Content>() = {}", std::mem::size_of::<serde::__private229::de::Content>());
let plain = r#"{"t":"Post","title":"hello","tags":["a","b","c"],"views":7}"#;
let escaped = r#"{"t":"Post","title":"he\"llo","tags":["a","b","c"],"views":7}"#;
for (name, input) in [("no escapes", plain), ("escaped strings", escaped)] {
println!("{name}: {input}");
measure::<Post>("plain struct", input);
measure::<Tagged>("tag = \"t\"", input);
measure::<Untagged>("untagged", input);
measure::<Flat>("flatten", input);
measure::<Outer>("tag wrapping untagged", input);
measure::<OuterNoHook>("same, hook hidden", input);
}
}
Post 直接读时 "t" 是未知字段,被忽略。最后一行用 NoHook 把里层面对的 ContentDeserializer 包起来:forward_to_deserialize_any! 生成的方法都转给 deserialize_any,__deserialize_content_v1 没有覆盖,落到默认实现,也是 deserialize_any。这样里层看不到钩子,用来对比。M2 Max、rustc 1.99.0,cargo run --release,跑了三次,分配那两列每次相同,耗时每次相差不到 15 ns,下面是第一次的前半段(后半段在文末):
size_of::<Content>() = 32
no escapes: {"t":"Post","title":"hello","tags":["a","b","c"],"views":7}
plain struct allocs=5 bytes=104 169 ns
tag = "t" allocs=7 bytes=464 284 ns
untagged allocs=7 bytes=464 283 ns
flatten allocs=7 bytes=464 275 ns
tag wrapping untagged allocs=7 bytes=464 268 ns
same, hook hidden allocs=8 bytes=656 311 ns
还是上面的 cost.rs,第二段输入只把 title 换成了带转义的 "he\"llo"。这一段里 plain struct 是 6 次、tag = "t" 是 8 次。untagged 是几次?tag wrapping untagged 呢?先猜,再看文末。
第一段输入(没有转义)的账:
- 直接读,5 次、104 字节。
title的String(5 字节)、三个 tag 的String(各 1 字节)、tags的Vec<String>(serde_json 的数组没有size_hint,Vec第一次push时按最小容量 4 分配,4 × 24 = 96 字节)。 - 缓冲,多 2 次、多 360 字节。
Content是 32 字节,一对键值 64 字节。整个对象的Content::Map一个Vec(untagged 是 4 个键,内部标签去掉标签剩 3 个,容量都是 4,256 字节),tags的Content::Seq一个Vec(容量 4,128 字节)。字符串都没有转义,在树里是Str,借用输入,不分配。重放时Vec<String>拿到的size_hint是精确的 3(SeqRefDeserializer知道还剩几个),按 3 预分配,比直接读还省了 24 字节:104 + 256 + 128 − 24 = 464。 - 耗时多六成多。 这里的输入很小,建树、遍历、多出来的两次分配占的比例就大。
- 内部标签套 untagged 和单独一层一样,7 次。 外层
TaggedContentVisitor建了一棵树;里层 untagged 用ContentVisitor再要一棵时,面对的「格式」是ContentDeserializer,它的__deserialize_content_v1直接把自己的树交出去。 - 把钩子藏起来,多 1 次、多 192 字节。 里层对
NoHook调deserialize_any,ContentDeserializer按visit_map把顶层重新喂给ContentVisitor,重建顶层Map的Vec:3 项(标签已被外层拿走),这次size_hint是精确的 3,3 × 64 = 192 字节。只重建了这一层:每个值仍由ContentDeserializer交给新的ContentVisitor,那里钩子又生效了,tags的Seq原样交出。钩子省下的是每一层「格式本身就是Content」时的重建。
「Obsoleted by #2912」
Content 定义上方那句「Obsoleted by format-specific buffer types (https://github.com/serde-rs/serde/pull/2912)」读起来像是已经被取代了,其实没有。#2912(「Customizable buffer type (proof of concept)」)在 2025-03-22 创建,到本文写作时(2026-10)还是一个未合并的草稿 PR。它的说明是:
This demonstrates a backward compatible way that a format such as serde_json could use its own serde_json::Value as its buffer type for untagged enums and flatten, instead of "Content", solving https://github.com/serde-rs/serde/issues/1183. Uses "return position impl trait in traits" (stabilized in Rust 1.75 by https://github.com/rust-lang/rust/pull/115822) and "precise capturing in traits" (stabilized in Rust 1.87 by https://github.com/rust-lang/rust/pull/138128).
草稿里的设计:Deserializer 加一个带默认实现的 deserialize_buffer(self) -> Result<impl Buffer<'de, Error = Self::Error>, Self::Error>,MapAccess 加一个 next_value_buffer;新 trait Buffer<'de> 有两个方法 owned_deserializer 和 ref_deserializer,正好对应今天的 ContentDeserializer 和 ContentRefDeserializer。默认实现仍然读成 Content,所以不改的格式行为不变;serde_json 可以返回自己的 Value,用它自己的 Deserializer 重放,MapKey 那种按提示解析数字键的行为就能保留下来。草稿里改了 untagged 和相邻标签两处生成代码,改成调这几个方法,不再直接写 Content。
1.0.229 里能看到的只是这个方向留下的两处痕迹:那句注释,和 __deserialize_content_v1 这个钩子。技术上格式可以覆盖后者,自己建一棵 Content 交出去(它是 #[doc(hidden)],注释写着「Not public API.」,前面查过的格式都没这么做),但交出去的仍然只能是 Content,重放的仍然是 serde 的两个 ContentDeserializer,上一节丢掉的东西一样都回不来。
问: serde 为什么不干脆用 serde_json::Value 这样现成的东西当缓冲,而要自己造一个 Content?
答: serde 不能依赖任何一个格式,缓冲类型只能在 serde 自己里面。而且它得装得下所有格式通过 deserialize_any 交出来的东西:Value 没有字节串、没有借用的 &'de str、分不清 u8 和 u64、没有 char,别的格式交出来的这些信息放进去就走样了。Content 就是「数据模型能表达的、deserialize_any 会给出的」那个最小公倍数。代价是它对每个格式都不够好:对 JSON 来说它不知道数字键、原始文本和任意精度。#2912 的思路就是反过来,让每个格式提供自己的缓冲类型。
动脑筋答案
cost.rs 第二段的完整输出:
escaped strings: {"t":"Post","title":"he\"llo","tags":["a","b","c"],"views":7}
plain struct allocs=6 bytes=113 186 ns
tag = "t" allocs=8 bytes=473 304 ns
untagged allocs=9 bytes=479 323 ns
flatten allocs=8 bytes=473 294 ns
tag wrapping untagged allocs=9 bytes=479 303 ns
same, hook hidden allocs=10 bytes=671 345 ns
plain struct比第一段多 1 次:serde_json 第一次遇到转义时给它的scratch缓冲分配空间("he\"llo"解码后 6 字节,写进scratch,再拷贝成String),字节数 104 → 113 多出的 9 字节就是scratch的容量 8 加上String长 5 变成 6 的那 1 字节。tag = "t"是 8 次:scratch同样多 1 次;解码后的字符串进Content时visit_str拷贝成Content::String,这一次顶替了直接读时String字段的那一次;重放时ContentDeserializer用visit_string把它整个移交给title,不再分配。untagged是 9 次:前面都一样,但重放用的是ContentRefDeserializer,它只借着这棵树,对Content::String只能visit_str,title的String要再拷贝一份,多 1 次、多 6 字节(473 → 479)。这就是上面那张表里「visit_string移交」和「visit_str再拷贝」的差别。tag wrapping untagged也是 9 次,和单独的 untagged 一样:外层的树由钩子原样交给里层,里层照 untagged 的方式借用重放。same, hook hidden是 10 次,比上一行多的 1 次和 192 字节,与第一段输入里一样,是重建顶层Map的那个Vec。
小结
Content<'de>是 22 个变体的缓冲树:没有u128/i128、没有 enum,struct、map 都成了Map(Vec存键值对),能借的字符串存成Str。定义在serde_core,只因为Deserializer的隐藏方法签名要用它;读写它的代码都在serde/src/private/de.rs。- 读入:
ContentVisitor作 seed 调__deserialize_content_v1,默认是deserialize_any。serde_json 1.0.154 没有覆盖,覆盖它的只有 serde 自己的两个ContentDeserializer(直接交出树或克隆),让缓冲套缓冲时不重建(#2148,1.0.133;拆 crate 时经 #2968 恢复,1.0.220)。 - 重放:
ContentDeserializer拿所有权用一次(内部标签、flatten 进 struct),ContentRefDeserializer借用可以试多次(untagged、flatten 进 map),后者对String只能visit_str,多一次拷贝。整数提示都映射到deserialize_integer,按实际变体回调;deserialize_option把非空内容都当Some。 - 丢掉的东西都有真实报错:
u128 is not supported;数字键invalid type: string "1", expected u32(#1183);arbitrary_precision下小数invalid type: map, expected f64;RawValue和 YAML 的 enum 标签;报错没有行列号;bincode、postcard 不支持deserialize_any。 - 代价:这个小例子里多 2 次分配、多 360 字节、耗时多六成多;带转义的字符串在 untagged 下再多一次拷贝。
- 「Obsoleted by #2912」指的是一个未合并的草稿 PR:让格式用自己的缓冲类型(如
serde_json::Value)代替Content。1.0.229 里它还没有发生。