Waveform Parser Library Benchmark
波形文件格式解读
IEEE 1364 标准 · 纯文本格式 · 逐行记录值变化
VCD 是 Verilog 仿真器的标准输出格式。它是纯 ASCII 文本,由 Header(元数据+信号定义)和 Body(时间戳+值变化)两部分组成。优点是人类可读、通用兼容;缺点是冗余度极高,176MB 的 VCD 压缩成 FST 仅 12.8MB(~14:1)。
/* ---- Header: 元数据 + 信号层级定义 ---- */ $timescale 1ns $end $scope module top $end $var wire 1 ! clk $end // '!' 是信号的短ID $var wire 8 " data[7:0] $end // '"' 对应 8-bit 总线 $upscope $end $enddefinitions $end /* ---- Body: 时间戳 + 值变化(仅记录变化的信号) ---- */ #0 // 时间 = 0ns 0! // clk = 0 (标量: 值+ID) b00000000 " // data = 0x00 (向量: b+二进制 空格 ID) #5 // 时间 = 5ns 1! // clk = 1 #10 0! // clk = 0 b11111111 " // data = 0xFF
GTKWave 原创 · 二进制分块格式 · 内置压缩+索引
FST 由 GTKWave 作者 Tony Bybell 设计,专为高效存储和随机访问优化。它将数据分成固定大小的时间块,每块独立压缩(zlib/LZ4/FastLZ),并维护索引表。支持按信号ID和时间范围精确读取,无需扫描整个文件。
/* FST 是二进制格式,以下为逻辑结构示意 */ [Header Block] magic: 0x1B465354 // "FST" 魔数 start_time: 0 // 起始时间戳 end_time: 1000000 // 结束时间戳 num_vars: 2000 // 信号总数 [Hierarchy Block] // gzip 压缩 scope "top" { var wire 1 "clk"; var wire 8 "data"; } [Value Change Block #0] // LZ4 压缩 time_section: [0, 5, 10, ...] // 本块时间戳数组 signal "clk": [0, 1, 0, ...] // 1-bit: 2bit编码 signal "data": [00, .., FF] // 动态别名去重 [Value Change Block #1] // 独立压缩 ... // 可跳过不需要的块 [Geometry Block] // 索引表 block_offsets: [0x100, 0x8000, ...] // 各块文件偏移
库全面对比
| Library | Language 语言 |
Format 格式 |
Read/Write 读/写 |
Multi-thread 多线程 |
I/O Model I/O 模型 |
API Style API 风格 |
Dependencies 依赖 |
|---|
每项测试运行 3 次取平均值 · 记录标准差与峰值内存
将整个波形文件从磁盘加载到内存,解析文件头(信号定义、时间精度)和全部值变化数据,构建完整的内存数据结构。这是最重的操作,衡量库的端到端解析能力。
bench_large.vcd (176 MB, 2000 signals, 200K timesteps)
# Python (vcdvcd) 示例 vcd = VCDVCD("bench_large.vcd") # ← 这一行就是 full_parse # 返回后: vcd.data 包含所有信号的全部值变化 // Rust (wellen) 示例 let wave = simple::read("bench_large.vcd")?; // ← mmap + rayon 多线程解析 // 返回后: wave.hierarchy() + wave.source() 包含全部数据
解析文件并提取所有信号的名称和层级路径,不读取值变化数据。对于 VCD,需要扫描 header 部分的 $scope/$var 定义;对于 FST,读取 Hierarchy Block 即可。
bench_large.vcd
["top.clk", "top.rst_n", "top.cpu.pc[31:0]",
"top.cpu.alu.result[31:0]", "top.mem.addr[15:0]",
... // 共 2000 个信号路径]
获取波形文件的起始和结束时间戳。仅 Python 端单独测试,Rust 端将此操作合并进 pipeline。对于 FST 格式,时间范围存储在 header 中可瞬时读取;VCD 格式需要扫描到最后一个时间戳。
已加载的波形对象
(start=0, end=199999) 时间单位由 timescale 决定
选取特定信号,读取其全部值变化记录。这是波形查看器最核心的操作。注意:Python 端和 Rust 端的查询规模不同,同语言内纵向对比更有意义。
signal "top.clk" value changes: t=0 → 0 t=5 → 1 t=10 → 0 t=15 → 1 ... // 共 200K 个值变化点
模拟真实使用场景的完整工作流:加载文件 → 枚举信号 → 获取时间范围 → 查询值。一次性完成,衡量库在实际使用中的综合性能。对于「先缓存再操作」的库(如 wellen),后续步骤几乎零开销;对于「每次重新 I/O」的库(如 vcdvcd),每步都重新扫描文件。
# pipeline 伪代码 wave = load("bench_large.vcd") # Step 1: full_parse signals = wave.get_signal_list() # Step 2: signal_list (t_start, t_end) = wave.get_time_range() # Step 3: time_range values = wave.query(signals[0:10], t_start, t_end) # Step 4: value_query # 计时: 从 load 开始到 query 结束的总耗时
性能测试结果 — 最快的库为基准 1x,其余显示相对倍数
| Library | Language | full_parse | signal_list | value_query | pipeline |
|---|
关键发现
wellen 通过 mmap + rayon并行 + wavemem LZ4 压缩存储,达到 1.2 GB/s VCD 吞吐量,比单线程 rust-vcd 快 ~10x。
三层架构叠加:mmap 零系统调用 + rayon 分块并行 + 紧凑编码减少内存带宽压力
vcd-ng 内含两套完全不同的解析器:标准 Parser(full_parse: 38.6s)和 FastFlow 零拷贝解析器(value_query: 163ms),差距 237 倍。
标准 Parser 用 io::Bytes 逐字节读取;FastFlow 用 1MB 缓冲区零拷贝解析值变化
wellen FST full_parse 的 "9 GB/s" 实为延迟加载(仅读 header),真实全量解析:fstapi 277ms, fst-reader 316ms。
wellen 源码注释: "fst never reads the full body (unless all signals are displayed)"
wellen/pywellen 一次加载,后续零 I/O;vcdvcd 每次操作都要重新扫描文件。这使得 pipeline 中差距比单次操作更悬殊。
vcdvcd pipeline (6.77s) ≈ full_parse (6.88s),因为每步都在重新扫描 176MB 文件
推荐选择
| Use Case / 使用场景 | Recommended | Reason / 理由 |
|---|---|---|
| Rust 波形查看器后端 | wellen | VCD+FST+GHW 统一接口,多线程,Surfer 生产验证 |
| Rust 通用 VCD 读写 | rust-vcd | 零依赖,Iterator 流式 API,8 位贡献者 |
| Rust 高性能 VCD 值过滤 | vcd-ng (FastFlow) | 零拷贝解析,2bit/值编码,数据段专用 |
| Rust 纯 FST 读写 | fst-reader + fst-writer | 纯 Rust,零 C 依赖,proptest 验证 |
| Rust 完整 FST 功能 | fstapi | GTKWave C FFI,并行写入,mmap |
| Python VCD 快速脚本 | vcdvcd | 零依赖,pip install,Pythonic API |
| Python FST 处理 | pylibfst | CFFI C 绑定,C 级性能 |
| C/C++ EDA 嵌入 | gtkwave/libfst | 业界标准,6 文件独立编译 |