Yandex 开源 YaFF:为 Protobuf 设计的零拷贝线格式,读取速度接近结构体
Yandex 开源了 YaFF(Yet another Flat Format),这是一个为 Protobuf 生态打造的高性能零拷贝线格式。它保持 .proto 文件作为单一真相来源,仅改变数据在内存中的布局。YaFF 提供四种布局——Fixed、Flat、Sparse 和 Dynamic,其中 Flat 布局的读取速度在 Yandex 的基准测试中达到原始 C++ 结构体的 1.2 倍以内,比 FlatBuffers 快约 3.8 倍,比 Protobuf 快约 22 倍。该格式已在 Yandex 的广告推荐系统生产环境中使用,实现了 10-20% 的 CPU 节省。
TLDR
YaFF 是 Yandex 开发的高性能零拷贝序列化库,现已开源(Apache 2.0),目前为 C++ 实现,版本 v0.1.0。
它保留了 .proto 文件作为唯一的模式定义来源,仅改变数据在内存中的物理布局。这意味着你可以沿用现有的 Protobuf 模式定义,无需维护独立的新模式。在 Yandex 的基准测试中,其 Flat 布局读取热数据的速度比 FlatBuffers 快约 3.8 倍,仅比原始 C++ 结构体慢 1.2 倍。
YaFF 的目标不是取代 Protobuf,而是提供一种替代的线格式。同样的 .proto 模式可生成类似 Protobuf 的 C++ API。读取无需解析步骤,字段可直接从内存缓冲区中获取。对于性能不敏感的代码,仍可将线格式解析回 Protobuf 消息。这种双向转换能力使得模块级逐步采用成为现实:你可以先在热路径中引入 YaFF,其余部分继续保持 Protobuf。
该格式解决了高负载后端中 Protobuf 解析可能消耗两位数百分比 CPU 的问题。常见的零拷贝方案 FlatBuffers 并非 Protobuf 的即插即用替代品,需要维护独立的模式定义和转换层,且语义不兼容。YaFF 填补了这一空白:在保留 Protobuf 语义的前提下实现零拷贝读取。
YaFF 提供了四种物理布局:Fixed 是纯打包结构体,无头部,模式冻结;Flat 增加两字节头部,支持一定的模式演进;Sparse 通过元表寻址字段,适合稀疏模式;Dynamic 是默认布局,运行时根据情况自动选择 Flat 或 Sparse。
Yandex 发布了可重现的基准测试套件,基于 google/benchmark。在 AMD EPYC 7713 上,热层级数据中 Flat 布局读取时间为 9.79 纳秒,FlatBuffers 为 37.30 纳秒,Protobuf 为 219.35 纳秒,原始 C++ 结构体基线为 8.14 纳秒。
YaFF 还通过编译注解解决了类型双关问题。FlatBuffers 和 YaFF 都通过重新解释内存来读取字段,但 LLVM 的别名分析可能认为重复读取不安全。YaFF 在生成的代码中添加注解,告诉编译器何时可以安全复用访问链,从而避免重复计算。
YaFF 适用于你能同时控制生产者和消费者的系统,例如推荐系统和广告服务后端。Yandex 的广告推荐系统已采用 YaFF,在生产规模下节省了 10-20% 的 CPU。内存映射索引也是一个典型应用场景:主机可以加载数十 GB 的本地数据,无需重启服务即可保留解析结果。规划中的列式布局将面向分析型和机器学习流水线中的大量重复字段。
使用 YaFF 相当简单:通过 CMake 或 Conan 集成,编译时先运行 protobuf_generate() 再运行 yaff_generate()。生成的类型位于 protoyaff:: 命名空间。大多数项目只需链接 yaff::core 和 yaff::proto。