Rust中Display trait与serde_json序列化性能差距悬殊的原因咨询
嘿,我看了你遇到的这个性能问题,确实挺让人挠头的——都是把结构化数据转成文本输出,怎么用Display trait的速度会比serde_json慢这么多呢?咱们来一步步拆解你的代码和两种实现的差异,应该能找到核心问题所在。
首先,你的Display实现里藏着一个大逻辑错误(也是性能杀手)
先看你写的循环:
for entry in &self.rib_entries { writeln!(f, "TIME: {}", self.time.format("%Y-%m-%d %H:%M:%S"))?; writeln!(f, "TYPE: TABLE_DUMP_V2/IPV4_UNICAST")?; writeln!(f, "PREFIX: {}/{}", self.prefix, self.prefix_len)?; writeln!(f, "SEQUENCE: {}", self.sequence_number)?; // ... 然后是entry的字段 }
你把TIME、TYPE、PREFIX这些属于整个RibIpV4Unicast的公共字段,放到了每个rib_entry的循环里面!这意味着每个entry都会重复输出一遍这些完全相同的内容——不仅生成了冗余的输出(看你的示例输出,原bgpdump格式里这些公共字段应该只针对每个prefix输出一次,再跟着多个entry的详情),还做了大量毫无必要的重复格式化工作:比如每次循环都要格式化self.time、每次都要写相同的TYPE字符串,这会直接把CPU开销和输出的行数放大N倍(N是entry的数量),性能能不差吗?
其次,频繁的writeln!调用带来的累积开销
你的代码里几乎每一行输出都用了一次writeln!,每次调用都会:
- 触发
fmt::Formatter的状态检查和处理 - 处理格式字符串的相关逻辑(即使是编译时检查过,函数调用开销依然存在)
- 即使是块缓冲的输出流,多次小写入的函数调用开销累加起来也非常可观
而serde_json的实现是批量处理数据:它会尽量把整个对象(甚至多个对象)组织成连续的字节块,再一次性写入输出流,极大减少了函数调用和I/O操作的次数。
第三,临时对象的创建导致额外内存开销
比如处理AS PATH和社区属性的时候,你用了这样的写法:
as_path.segments.iter().map(|seg| seg.to_string()).collect::<Vec<_>>().join(" ")
这里会先把每个segment转成字符串,收集到一个临时Vec<String>里,再调用join生成最终的字符串。这个过程会产生多次内存分配、拷贝,而serde_json在序列化数组的时候,会直接逐个写入元素和分隔符,完全不需要创建中间的容器对象,内存开销小很多。
给你的优化建议
- 先修复逻辑错误,减少重复工作
把公共字段的输出移到entry循环外面,只执行一次:
impl Display for RibIpV4Unicast { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // 公共字段只输出一次 writeln!(f, "TIME: {}", self.time.format("%Y-%m-%d %H:%M:%S"))?; writeln!(f, "TYPE: TABLE_DUMP_V2/IPV4_UNICAST")?; writeln!(f, "PREFIX: {}/{}", self.prefix, self.prefix_len)?; writeln!(f, "SEQUENCE: {}", self.sequence_number)?; for entry in &self.rib_entries { // 只输出entry独有的字段 writeln!(f, "FROM: {} AS {}", entry.peer_ip, entry.peer_asn)?; writeln!(f, "ORIGINATED: {}", entry.originated_time.format("%Y-%m-%d %H:%M:%S"))?; if let Some(origin) = &entry.bgp_origin { writeln!(f, "ORIGIN: {}", origin)?; } // 优化AS PATH的处理,避免临时Vec if let Some(as_path) = &entry.bgp_as_path { write!(f, "ASPATH: ")?; let mut first = true; for seg in &as_path.segments { if !first { write!(f, " ")?; } write!(f, "{}", seg)?; first = false; } writeln!(f)?; } // 其他字段同理优化... writeln!(f)?; } Ok(()) } }
这一步应该能直接把性能提升好几倍,因为你去掉了90%以上的重复格式化和写入操作。
减少临时对象的创建
像上面优化AS PATH的写法那样,用循环直接写入每个元素,避免collect和join带来的中间容器。尽量合并写入操作(可选)
如果追求极致性能,可以把多个字段的写入合并成更少的write!调用,比如:
// 把entry的多个字段合并成一次write write!(f, "FROM: {} AS {}\nORIGINATED: {}\n", entry.peer_ip, entry.peer_asn, entry.originated_time.format("%Y-%m-%d %H:%M:%S"))?;
这样能减少函数调用的开销,但会牺牲一点代码可读性,你可以根据需求权衡。
最后总结一下差距的核心原因
serde_json是一个高度优化的序列化库,它的实现针对批量数据处理做了大量优化:减少函数调用、避免不必要的内存分配、批量写入字节。而你的Display实现不仅有逻辑错误导致的重复工作,还做了很多低效的小写入和临时对象创建,这些累加起来就造成了巨大的性能差距。
按照上面的建议优化后,应该能把Display的性能提升到和serde_json接近的水平(当然,因为文本格式的行数更多,可能还是会慢一点,但不会差一个数量级)。
内容来源于stack exchange

