Serde JSON反序列化`{c:[{}]}`为RawValue为何产生堆分配?
为什么会产生意料之外的堆分配
这一分配来自serde_json的嵌套结构跟踪逻辑,是框架为了避免递归解析JSON触发栈溢出做的设计:
- 反序列化
&RawValue类型时,必须先扫描对应JSON片段的起止边界,这个过程需要匹配[]、{}的嵌套关系来确定结束位置 - serde_json没有使用函数递归处理嵌套,而是用
Vec作为迭代扫描的状态栈,Vec默认初始化时不会分配内存,仅在第一次插入元素时触发堆分配 - 当
RawValue内容为[2, 3]时,数组内部都是基本类型,扫描过程中没有遇到新的嵌套复合结构,不需要向状态栈插入元素,因此不会产生分配 - 当
RawValue内容为[{}]时,数组内部嵌套了对象,扫描到{时需要向状态栈插入标记记录嵌套层级,这就触发了Vec的第一次堆分配
零堆分配调整方案
有两种可落地的方案:
方案1:启用serde_json的smallvec特性(无侵入,推荐)
serde_json官方提供了smallvec特性支持,开启后嵌套状态栈会改用SmallVec实现,默认在栈上预留了128层嵌套的存储空间,只要你的JSON嵌套深度不超过128,全程不会触发堆分配。
仅需修改Cargo.toml中的serde_json依赖声明即可,不需要修改任何业务代码:
serde_json = { version = "1.0", features = ["raw_value", "smallvec"] }
方案2:手动解析边界(极致性能场景)
如果你确定输入JSON结构固定,可以手动扫描外层结构切出c字段对应的切片,再转换为&RawValue,完全绕开serde_json的嵌套扫描逻辑,天然零分配:
#[derive(Deserialize, Debug)] struct MyStruct<'a> { #[serde(borrow)] c: &'a serde_json::value::RawValue, } fn main() { let msg = r#"{"c":[{}]}"#; // 示例逻辑:手动定位c字段值的起止位置,生产环境使用需要补充通用扫描和边界校验 let c_slice = &msg[5..msg.len() - 1]; let c = serde_json::from_str::<&serde_json::value::RawValue>(c_slice).unwrap(); let my_struct = MyStruct { c }; }
内容的提问来源于stack exchange,提问作者Dan
相关产品推荐
相关产品推荐

