本文首发公众号 猩猩程序员 欢迎关注
本文来自 www.figma.com/blog/suppor…
使用 Rust 内存优化提升文件加载速度
Raghav Anand,Figma 软件工程师
Figma 工程团队|质量与性能
🎨 插图作者:Jose Flores
两个色彩斑斓的碗——一个混乱而溢出,另一个整齐堆叠——盛满了几何形状的水果和蔬菜,象征内存使用的混乱与优化。
内存效率对于良好的用户体验至关重要。为了保持文件的快速加载和高性能,Figma 团队持续在寻找优化点——以下是我们在 Rust 中的一些优化实践。
文件加载背后的内存结构
Figma 的多人协作系统负责加载文件、同步多个协作者的更新,以及定期保存文件状态的快照。为了让实时协作在复杂的 Figma 文件结构图上高效运作,我们会将大量文件内容加载到内存中。
随着 Figma 的不断发展,我们在寻找能够高效扩展且不牺牲用户体验的方法。引入动态页面加载后,我们服务器端需要解码的文件数量激增了 30%。为应对这一新增负载,我们在 Rust 中探索了多项性能优化,取得了更快的加载速度与更高的内存效率。
更小、更高效的内存映射结构
一个 Figma 文件本质上是由一系列节点组成的集合。每个节点代表一个对象,例如三角形、方块、画板等;每个节点可以看作是一组属性的集合,例如颜色、形状、父节点等等。
在服务器端,我们将节点表示为一个键值映射结构:Map<属性名(u16), 属性值(u64指针)>,其中 u16 和 u64 分别是键值的比特宽度。
这个映射结构位于文件加载的关键路径上,任何在此处的加速都能直接带动整体文件加载速度的提升。同时,内存分析也表明这个 map 占据了单个文件内存使用的 60% 以上(考虑到它只是存元数据,这结果让我们颇为惊讶)。
我们原先使用 Rust 默认的 BTreeMap 实现,因为我们的序列化协议需要有序迭代。但仔细研究后我们意识到:由于键的范围非常小,我们其实并不需要通用 map 的所有特性。
键空间非常有限
Figma 文件的字段(属性)定义在 schema 中,而该 schema 的演进速度与人类速度一致——非常慢。当前 schema 字段数量少于 200 个,并且这些字段呈现聚集状态。例如,只有评论节点才会使用“评论相关字段”,其他节点不会用到。
我们在实际文件上统计后发现:每个节点的属性平均只有大约 60 个。
使用更简单的 Vec 结构
基于此发现,我们将 BTreeMap 替换为更简单、内存效率更高的结构:排好序的扁平向量(vector) 。
原结构如下:
BTreeMap<u16, pointer>{
0: 1, // node_id
1: FRAME, // 类型
2: null, // parent_id
... (x, y, w, h 等)
}
优化后结构为:
Vec<(u16, pointer)>{
(0, 1),
(1, FRAME),
(2, null),
... (x, y, w, h)
}
理论上,使用向量结构的操作复杂度如下表:
| 操作 | BTreeMap | Vec |
|---|---|---|
| 插入 | O(log n) | O(n) 最坏 |
| 查找 | O(log n) | O(n) |
| 修改 | O(log n) | O(n) |
看起来似乎向量更慢。但实际上,由于计算机在处理线性、小量内存时非常高效(恰好适配我们的数据结构),所以实际文件反序列化速度更快。而且对于大型文件,内存使用下降了近 25% ,在生产环境中取得了显著收益。
用位填充(bit stuffing)进一步节省内存
虽然我们部署了上述优化,但我们还研究了一项尚未上线的额外优化。
我们再次审视 Map<u16, pointer>,提出问题:指针究竟是什么?
通常,指针是 64 位数据,用于告诉 CPU 数据的起始地址。64 位可表示 2⁶⁴ 字节,也就是 18 EB(前所未有的量级)。实际上,x86 架构通常只使用其中的 48 位来寻址。
因此,指针的高 16 位通常是空的,可以用作其他用途:
[0_u16, pointer_u48]
恰好,我们的 Map<u16, pointer> 中需要额外的 16 位存储字段 ID。因此,我们可以将字段 ID 与指针打包进一个 64 位整数中!
优化后的数据结构如下:
Vec<u64>{
[0_u16 << 48 | ptr_u48],
[1_u16 << 48 | FRAME_ptr_u48],
...
}
我们通过位操作(bitwise operations)从中提取字段 ID 和实际指针。这种结构虽然更复杂,需要精细管理生命周期(例如处理引用计数、避免内存泄漏),但它确实带来了进一步的提升:
- 性能略有提升
- RSS(常驻内存)下降约 5%
虽然我们原本期待节省 20%(因为只用 64 位而不是两个值),但实际 RSS 受操作系统和内存分配器的影响较大,不能简单地从分配量推导。
最终,我们决定不将此优化投入生产,原因很简单:收益不足以抵消潜在的稳定性风险。但如果未来需要,我们仍可启用这一策略。
成果:数字在下降!
使用 vector 代替 map 后,Rust 中的 Figma 文件加载速度显著提升:
- 在 P99 文件加载延迟中,反序列化时间缩短了 20% 。
- 整体多人协作系统内存成本下降了 20% 。
我们始终致力于让 Figma 的多人协作技术变得更高效、更易维护、更有性能。如果你对这样的优化感兴趣,欢迎加入我们!
本文首发公众号 猩猩程序员 欢迎关注