You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust中Display trait与serde_json序列化性能差距悬殊的原因咨询

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在序列化数组的时候,会直接逐个写入元素和分隔符,完全不需要创建中间的容器对象,内存开销小很多。

给你的优化建议

  1. 先修复逻辑错误,减少重复工作
    把公共字段的输出移到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%以上的重复格式化和写入操作。

  1. 减少临时对象的创建
    像上面优化AS PATH的写法那样,用循环直接写入每个元素,避免collect和join带来的中间容器。

  2. 尽量合并写入操作(可选)
    如果追求极致性能,可以把多个字段的写入合并成更少的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:18:02