提速40倍!bug更少了,我们用Rust重写了自家项目

图片
作者 | Peefy
译者 | 核子可乐
策划 | 刘燕
如今,Rust 已经悄然成为最具人气的编程语言之一。作为一门新兴的系统语言,Rust 天然具备内存安全机制、与 C/C++ 相近的性能优势、出色的开发者社区,以及完备的说明文档、工具链和 IDE 体验等。
在本文中,我们将介绍如何使用 Rust 改写项目,逐步实现生产环境。讲解改写中为何选择 Rust,实际遇到的问题以及使用 Rust 改写后的真实效果。
我们用 Rust 开发的这个项目名叫 KCL。KCL 是一种基于约束的记录与函数开源语言。KCL 通过成熟的编程语言技术和实践改进了大量复杂的配置编写,强调以配置为基础构建起更好的模块化、扩展性和稳定性,降低逻辑编写难度,同时改善自动化与生态扩展能力。
KCL 之前一直用 Python 进行编写。
考虑到用户体验、性能和稳定性等因素,我们决定用 Rust 进行重写,并由此获得了以下提升:
凭借 Rust 强大的编译检查和错误处理功能,现在 KCL 的 bug 更少了。
语言端到端编译和执行性能提高了 66%。
语言前端解析器性能提升达 20 倍。
语言语义分析器的性能提高达 40 倍。
语言编译器在编译过程中,平均内存使用量仅为原始 Python 版本的一半。
我们遇到了哪些问题?
编译器、构建系统和运行时都能在 Rust 社区里找到定位相近的现成方案,例如 deno、swc、turbopack 和 rustc 等。我们使用 Rust 完整构建了编译器的前、中和运行时,取得的效果比一年前用 Python 的时候更好。
一年前,我们使用 Python 构建了 KCL 编译器的整个实现。虽然刚开始跑得不错,Python 简单易用、生态丰富且团队研发效率也高,但随着代码膨胀和工程师数量的增加,Python 代码的维护开始变得愈发困难。
虽然我们强制要求在项目中编写 Python 类型注释,也采用了更严格的 lint 工具,并将代码测试行的覆盖率设定在了 90% 以上,但仍然挡不住种种运行时错误。比如 Python None 空对象、找不到属性等等。我们在重构 Python 代码时总得小心谨慎,这已经严重影响到了用户体验。
此外,由于 KCL 的用户以开发者为主,所以编程语言或编译器的任何内部实现错误都是不可接受的,这同样给我们的用户体验带来了大麻烦。Python 编写的程序启动缓慢,性能也无法满足自动化系统在线编译执行的效率需求。因为在我们的场景中,用户希望能在修改 KCL 代码之后,快速查看编译结果。很明显,用 Python 编写的编译器没办法满足这方面的要求。
为什么选择 Rust
我们选择 Rust 的理由如下:
我们曾使用 Python、Go 和 Rust 实现了一个简单的编程语言栈虚拟机,并进行了性能比较。在此场景下,Go 和 Rust 的性能差不多,Python 则明显落后一些。综合考虑之后,我们决定选择 Rust。三种语言的栈虚拟机实现代码在此: https://github.com/Peefy/StackMachine。
越来越多的编程语言编译器或运行时,特别是前端基础设施项目,开始用 Rust 进行编写或重构。此外,Rust 还广泛出现在基础设施、数据库、搜索引擎、网络、云原生、UI、嵌入式等领域。从这个角度看,Rust 语言的可行性和稳定性至少有所保障。
考虑到后续项目开发将涉及到区块链和智能合约方向,我们发现社区中有大量区块链和智能合约项目都是用 Rust 编写的。
使用 Rust 可以带来更好的性能和稳定性,让系统更易于维护且更加健壮。另外,Rust 还可以通过 FFI 对外开放 C API,借此实现多语言使用与扩展、方便生态融合。
Rust 能够很好地支持 WASM。目前社区中有很多 WASM 生态系统都是由 Rust 构建而成。KCL 语言和编译器可以由 Rust 编译为 WASM,进而在浏览器中运行。
基于上述理由,我们最终选择了 Rust 而非 Go。在整个重写过程中,我们发现 Rust 的综合素质确实非常优秀(性能高、抽象度好)。虽然某些语言特性还不完善,比如生命周期上会带来一定额外成本且生态不够丰富,但 Rust 仍给我们留下了深刻印象。
使用 Rust 有哪些难点
虽然我们决定用 Rust 重写整个 KCL 项目,但大多数团队成员还没有用 Rust 编写项目的经验。我之前学过 Rust 语言,依稀记得大概学到 Rc 和 RefCell 就放弃了,当时也从没想到 Rust 会跟 C++ 有那么多相似之处。
使用 Rust 的风险,主要体现在 Rust 语言的学习成本上,不少博文都提到过这个问题。由于 KCL 项目的整体架构并没有太大变化,只是针对 Rust 做出子正经模块设计和编码优化,所以我们的重写工作基本就是以边做边学的方式在进行。
刚开始使用 Rust 编写整个项目时,我们花了不少时间研究查询、编译和调试方面的知识。但随着项目推进,我们发现 Rust 开发的最大难题主要集中在思路转变和开发效率上。
思路转变
首先,Rust 的语法和语义很好地吸纳并整合了函数式编程中的类型系统相关概念,例如抽象代数类型(ADT)。
另外,Rust 中没有“继承”的概念。如果没法理解这些,即使是其他语言中非常普通的结构定义,在 Rust 中也可能会占用我们大量时间。例如,以下 Python 代码在 Rust 中就需要改写成后面的形式:
Python
Rust
当然,我们在应对 Rust 编译器的错误报告方面也花了不少时间。Rust 编译器确实经常给开发者“找麻烦”,比如抛出借用校验错误。特别是 KCL 编译器,其核心结构是抽象语法树(AST),一种递归嵌套的树结构。
Rust 中有时很难考虑到变量可变性和借用检查之间的关系。以 KCL 编译器中定义的范围结构 Scope 为例,对于存在循环引用的场景,我们要使用 Scope 来展示需要关注的数据依赖性,同时广泛使用 Rust 中的智能指针结构,例如 Rc、RefCell 和 Weak。
开发效率
Rust 的开发效率可以说是“先抑后扬”。在项目重写之初,如果团队成员之前没接触过函数式编程的概念,不熟悉相关习惯,那 Rust 的开发速度要明显慢于 Python、GO 和 Java 等语言。然而一旦大家熟悉了 Rust 标准库的常见用法和最佳实践,搞定了 Rust 编译器抛出的错误报告,开发效率就会大大提高。在上手之后,Rust 其实更易于写出高质量、安全且高效的本机代码。
例如,我们曾遇到如下所示的 Rust 生命周期错误。在排查了很久之后,我们才发现是忘记标注生命周期参数所引发的生命周期不匹配。此外,Rust 的生命周期与类型系统、作用域、所有权和借用检查等概念耦合起来,确实带来了更高的理解成本和复杂度,其报错信息也不及类型错误那么明显。生命周期不匹配报错中的信息也有点死板,有时会给故障排查增加难度、带来高昂的纠错成本。当然,在熟悉这些概念之后,效率都能有所提高。
使用 Rust 的重写收益
经过几个月时间,团队成员们终于用 Rust 完成了项目重写并稳定投入生产环境。回顾整个过程,我们觉得颇有收获。从技术角度来说,重写的过程不仅让我们快速掌握了一门新的编程语言和相关知识,并积极付诸实践。而且,整个重写过程也让我们反思了 KCL 编译器的不合理性并做出修改。在这样一个长周期项目之后,我们发现编译系统更稳定、更安全、代码更清晰、bug 更少而且性能也更高了。
虽然并不是所有模块都实现了 40 倍提速(毕竟有些模块的性能瓶颈不在语言上,比如 KCL 运行时就是内存深拷贝操作),但付出的这些也还是值得的。在使用了一段时间 Rust 之后,思维习惯和开发效率也都不再是问题。
总结
我个人认为,用 Rust 改写项目的最大收获就是让自己又掌握了一门新的编程语言,同时也让我们有机会接触并使用这种势头强劲的语言选项。
在 Rust 的帮助下,我们的 KCL 语言和编译器更加稳定、启动速度和自动化效率都上了新的台阶。KCL 的性能已经优于社区中的其他同类语言,为用户体验带来了显著提升。而这一切都利益于 Rust 的无 GC、高性能、完善错误处理、内存管理、零抽象等特性。各位用户朋友,这是专为你们做的!
最后,如果大家关注 KCL 项目,想要用 KCL 构建自己的场景,或者打算使用 Rust 参与开源项目,欢迎访问https://github.com/KusionStack/community
加入我们的社区大家庭。
原文链接:https://medium.com/@xpf6677/40x-faster-we-rewrote-our-project-with-rust-120b006c6abe