查看原文
其他

我们用 Rust 重写了项目,快了将近 40 倍

为开发者服务的 21CTO 2024-04-14

导读:以下是选择 Rust 的原因,包括我们遇到的障碍,以及重写后的收益比例

Rust 已经悄然成为最流行的编程语言之一。

其作为一门新兴的系统语言,具有内存安全机制、接近C/C++的性能优势、优秀的开发者社区、工具链、IDE等诸多特点。

本文将介绍我们用 Rust 重写项目并逐步在生产环境中实现的过程,包括选择 Rust 的原因、遇到的问题和结果。

我们正在使用 Rust 开发的项目称为KCL。KCL是一种开源的基于约束的记录与功能语言。它通过成熟的编程语言栈改进了复杂配置的编写。它的目标是围绕配置构建更好的模块化、可扩展性和稳定性、更简单的逻辑编写,快速的自动化和良好的生态扩展性。

KCL项目之前是用Python写的。考虑到用户体验、性能和稳定性,我们决定用 Rust 重写,收获了如下益处:

  • Rust 强大的编译检查和错误处理,让Bug更少;

  • 语言中端到端编译和执行性能提高 66%;

  • 语言前端解析器性能提升20倍;

  • 语言语义分析器的性能提升了 40 倍;

  • 语言编译器在编译过程中的平均内存占有量是原来 Python 版的一半。


遇到的问题


关于编译器、构建系统和运行时也用 Rust 在相同类型的项目中做同样的事,比如deno、swc、turbopack、rustc。我们使用 Rust 来构建编译器的前端、中间和运行时。


一年前,我们使用 Python 构建了 KCL 编译器的整个实现。虽然一开始跑的还不错,但是由于Python简单易用,团队的研发效率也很高。但是随着代码的扩充和工程师数量的增加,代码维护变得困难。


被迫无奈,我们在项目中写了 Python 的类型注释,并采用了更严格的 lint 工具,这样代码测试行的覆盖率也达到了 90% 以上。但是,仍然有很多运行时错误,比如 Python None 空对象、空属性没被发现等等。


在这里也希望大家在重构 Python 代码时一定要小心,这会严重影响用户体验。


随着KCL 开发者越来越多,编程语言或编译器内部出现任何错误都变得无法容忍,从而导致影响自身都有很多用户体验的问题。


Python开发的程序启动慢,我们对自动化系统、对在线编译和执行效率需求也越来越高。在我们的场景中,需要在修改KCL代码后能够很快展示编译后的结果。


而这些用 Python 开发的编译器无法有效地满足这些要求。


为什么使用Rust?


我们选择 Rust 的原因总结一下。


我们使用 Python、Go 和 Rust 实现了一个简单的编程语言堆栈虚拟机,并比较了它们的性能。


在这种情况下,Go 和 Rust 的性能相似,而 Python 则表现出明显的性能差距。经过深思熟虑,我们选择了 Rust。


3种语言实现的栈虚拟机代码详情,是在这里:

https: //github.com/Peefy/StackMachine
  • 越来越多的编程语言编译器或运行时,尤其是前端基础设施项目,都在使用 Rust 编写或重构。此外,Rust还出现在基础设施、数据库、搜索引擎、网络、云原生、UI、嵌入式等领域,这些均验证了这个编程语言的可用性和稳定性。


  • 考虑到后续项目开发还会涉及到区块链和智能合约方向,用户社区中大量的区块链和智能合约项目也都是用Rust写的。


  • 通过 Rust 可以获得更好的性能和稳定性,使系统更易于维护和更健壮。同时可以通过FFI对外暴露C写的 API,实现多语言使用和扩展,方便生态扩展与融合。


  • Rust 用友好的方式支持 WASM。Rust 在社区中构建了大量的 WASM 生态系统。KCL 语言和编译器可以借助 Rust 编译成 WASM,并在浏览器中运行。


总结上述的原因,我们选择了 Rust 而不是 Go 语言。

特别是在整个重写过程中,我们发现Rust的综合素质确实非常优秀(主要是高性能和足够的抽象)。虽然在一些语言特性上有一定的学习成本,尤其是生命周期,但是它在生态上更加丰富。

使用 Rust 的困难


虽然我们决定用 Rust 重写整个 KCL 项目,但大多数团队成员都没有用 Rust 开发某个项目的经验,而我也只学过《The Rust Programming Language》。依稀记得在学习和等智能指针的时候就放弃Rc了RefCell。


当时,没想到Rust里面会有类似C++的东西。


使用Rust的风险主要是语言学习的成本,这在21CTO的其它文章都有提到(Rust 能为前端和 Web 开发带来了什么)。由于 KCL 项目的整体架构没有太大变化,一些模块设计和代码编写针对Rust也进行了优化,回此整个重写都是在边学边练的过程中进行的。


刚开始使用Rust编写整个项目时,我们在知识查询、编译、调试上花费了不少时间。但是随着项目的推进,在使用Rust的体验中遇到的困难主要是自我心智和开发效率的转变和升级。


心理的转变


首先,Rust 的语法和语义很好地吸收和整合了函数式程序中类型系统相关的概念,例如抽象代数类型(ADT)。


此外,Rust 中没有与“继承”相关的概念。如果你需要帮助理解它,即使是普通的结构定义,在 Rust 中也可能比在其它语言中花费更多时间。例如,以下 Python 代码将在 Rust 中有不同的定义:

from dataclasses import dataclass
class KCLObject: pass
@dataclassclass KCLIntObject(KCLObject): value: int
@dataclassclass KCLFloatObject(KCLObject): value: float

rust

enum KCLObject {      Int(u64),      Float(f64),}

开发效率


Rust的开发效率可谓是“先约束后提升”。手写项目初期,如果团队成员没有接触过函数式编程和相关的编程习惯,开发速度会明显慢于Python、Go、Java等语言。


但是,一旦他们熟悉了Rust标准库的常用方法和最佳实践,熟悉了Rust编译器的常见错误修改,开发效率将大大提高,可以原生编写出优质、安全、高效的代码。

例如,我遇到了 Rust 生命周期错误,如下代码所示。排查了半天,发现lifetime不匹配是因为忘记标注lifetime参数导致的。

此外,Rust 的生命周期与类型系统、范围、所有权和借用检查等概念相结合,导致理解成本高且复杂。错误报告信息通常不像类型错误那样明显。

生命周期不匹配错误报告信息有时是不灵活的,这可能导致高昂的故障排除成本。当然,熟悉相关概念后效率会提高。

struct Data < 'a > { b: & 'a u8 , }
// func2 省略了生命周期参数,func2 没有。// 默认情况下,Rust 编译器会将 func2 的生命周期推导为 '_,这会导致生命周期不匹配错误。impl < 'a > Data< 'a > { fn func1 (& self ) -> Data< 'a > {Data { b: & 0 }} fn func2 (& self ) -> Data {Data { b: & 0 } } }

使用Rust的收获

几位团队成员花了数月的时间,使用Rust完全重写并稳定投入到生产环境。几个月后,我们回顾了整个过程,觉得有如下的收获。

从技术角度来说,rewrite 的过程训练了我们快速学习一门新的编程语言和编程知识并付诸实践。整个重写过程让我们反思KCL编译器的设计不合理,并进行修改。对于一门编程语言来说,这是一个长周期的项目。我们了解到,编译系统更加稳定安全,代码清晰,bug更少,性能更好。

虽然不是所有的模块都能得到40倍的性能(因为有些模块的性能瓶颈,比如KCL运行时就是内存深拷贝操作),我觉得还是值得的。而当Rust使用到一定时期后,心智和开发效率就不再是限制因素。

结论


使用 Rust 重写项目的最重要的结果不仅仅是我学习了一门新的编程语言,或者 只是看Rust 是一种非常流行的语言,它能让我们写出花哨的代码。


相反地,使用 Rust 让 KCL 语言和编译器更加稳定,消除了启动速度和自动化的效率问题,并提高了 KCL 在类似领域中其他编程语言的低性能。


这些优点都归功于 Rust 的无 GC、高性能、更好的错误处理、内存管理、零抽象等优质特性。


总之,用户是第一受益者。


最后,如果各位喜欢KCL项目,想把KCL用于自己的场景,或者想用Rust参与开源项目,欢迎访问https://github.com/KusionStack/community加入我们的社区参与讨论与共同建设。


作者:高朋

参考资料

  • https://github.com/KusionStack/KCLVM

  • https://github.com/Peefy/StackMachine

  • https://doc.rust-lang.org/book/

  • https://github.com/sunface/rust-course

相关阅读:

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存