我跟老张说你的 Rust 服务可能慢了十倍,他差点把咖啡喷我脸上
Table of Contents
Rust Serde 宏让你的服务慢了十倍?老张的故事
昨天下午,老张抱着电脑跑到我工位旁边,一脸便秘的表情。
“兄弟,你帮我看看,我这个 Rust 服务怎么回事,明明代码写得好好的,怎么一上线就慢得跟老牛拉破车似的。”
我接过他的电脑,随便翻了翻代码。说实话,第一眼看上去没什么问题。Rust 么,天生就快,这事儿大家都知道。就像博尔特天生跑得快,你让他慢下来都难。
“你先说症状。“我对老张说。
“就是那个调试日志的功能,“老张指着一段代码,“产品说要记录每个请求的 payload,方便以后排查问题。我想着这简单啊,加个 serde_json::to_string 不就完事了。”
我凑近一看,好家伙,这货在每个请求处理流程里都做了一次 JSON 序列化。
“你这个服务每秒处理多少请求?“我问。
“测试环境压过,五万左右吧。“老张说。
我沉默了一会儿,然后说:“老张啊,你有没有想过,你这五万次请求,每次都要把整个数据结构序列化一遍,相当于什么?”
老张愣住了。
我给他打了个比方:“想象一下,你有个快递站。平时呢,你只需要看一眼快递单上的地址,然后扔到对应的篮子里就行了。这个速度很快,对吧?”
老张点点头。
“现在呢,“我继续说,“你为了’方便以后查阅’,每次收到一个快递,你都要把它拆开,把里面的东西一样一样拿出来拍照,然后再重新包好。你说这能不慢吗?”
老张的表情从困惑变成了恍然大悟。

“这就是 Serde 宏的问题,“我接着解释,“你看你这行代码,#[derive(Serialize, Deserialize)],一眼看过去没啥毛病。但这个宏在编译的时候会生成一大堆代码,这些代码在运行的时候要做很多事情。”
“比如什么?“老张开始认真听了。
“比如说,你的结构体里有个 String,序列化的时候会复制一份。有个 Vec 里面一万个数字,序列化的时候要给每个数字转成字符串,还要分配内存存这个 JSON 数组。这些操作看着不起眼,但你五万次请求一秒,那内存分配的频率就高得离谱了。”
我给老张看了一段代码:
use serde::{Serialize, Deserialize};
use serde_json;
#[derive(Serialize, Deserialize, Debug)]
struct Payload {
id: u64,
name: String,
values: Vec<u64>,
}
fn main() {
let payload = Payload {
id: 42,
name: "SuperLongStringThatKeepsAllocating".to_string(),
values: (0..10_000).collect(),
};
// 看起来很无辜的序列化
for _ in 0..1000 {
let _ = serde_json::to_string(&payload).unwrap();
}
}
“你看,每次循环都要:分配内存给整个 Vec
老张摸了摸下巴:“那怎么办?我不能不加日志啊。”
“当然不是让你砍掉功能,“我说,“你想想,你真的需要在每次请求处理的时候都序列化吗?”
老张想了一会儿:“也不是,其实我只需要在出错的时候记录一下就行。”
“对啊,那你为什么不把序列化这个操作移到出错的分支里呢?正常情况下根本不执行,只有出错了才序列化一次,这样不就快多了。”
老张眼睛一亮:“有道理。”
“还有,“我补充道,“你看看你这个结构体,里面好几个 String,其实你完全可以改成 &str,这样序列化的时候不用复制,直接借用原来的引用就行。”
我给他写了个优化版本:
#[derive(Serialize, Deserialize)]
struct Payload<'a> {
id: u64,
name: &'a str, // 用引用代替 String
}
“这就好比…“老张思考着。
“好比你要复印文件,“我接话,“String 就相当于每次都要把原件拿过来重新复印一份。&str 呢,就是直接拿原件给别人看,不用复印。你说哪个快?”
“懂了懂了,“老张连连点头,“还有别的招吗?”
“有啊,“我说,“你想想,你这日志是给自己看的,不需要给别的系统解析,那你干嘛非要用 JSON?用 Bincode 之类的二进制格式,速度快多了。”
“Bincode 是啥?”
“就是一种二进制序列化格式,比 JSON 快多了,大概能快个五六倍那样。“我给他演示了一下:
// 用 Bincode 代替 JSON
let encoded = bincode::serialize(&payload).unwrap();
“就好比你记笔记,用汉字写和用五笔输入法打字,当然是打字快啊。”
老张听完,马上回去改代码了。
大概过了半小时,老张又跑过来了,这次脸上笑开了花。
“兄弟,神了!我把序列化移到错误分支,把 String 改成 &str,然后换成 Bincode,一压测,性能直接回来了,CPU 占用还降了一大半。这性能优化效果太明显了。”
我拍拍他的肩膀:“记住了啊,Rust 本身是快,但再快的车也怕你往后备箱里塞两吨砖头。Serde 宏不是不好,它是个好东西,但你得知道它在干什么。”
老张连连点头:“学到了学到了。”

最后总结一下,如果你们也遇到类似的问题,可以这么排查:
第一步,确保是用 release 模式编译的,debug 模式慢是正常的。
第二步,用 cargo flamegraph 看看 CPU 时间都花在哪了,如果看到 serde_json 或者各种 clone 函数占比很高,那基本就是这个问题了。
第三步,检查一下你的代码,是不是在不需要序列化的地方也做了序列化。如果数据只是在你程序内部传递,那就直接传引用,别搞 JSON 那套。
第四步,如果确实需要序列化,考虑一下用更快的格式,比如 Bincode、MessagePack 之类的。
第五步,不要动不动就给整个大结构体加 Serialize、Deserialize,搞个轻量级的 DTO 专门用来序列化,只包含需要序列化的字段。
写代码就是这样,看似简单的操作背后可能隐藏着巨大的开销。Serde 宏用起来很爽,像吃自助餐一样随便拿,但你得知道,拿多了是要胖的。
如果这篇文章对你有帮助
觉得这篇文章有用的话,点个赞让更多人看到。有问题或者想交流的,欢迎在评论区讨论,或者直接关注我的博客,以后有更多好玩的 Rust 故事分享给你们。转发给需要的朋友,大家一起避坑。