快 40 倍! 我们用 Rust 重写了我们的项目!- Peefy


本篇博客将介绍使用Rust改写项目并逐步实现生产环境的过程,以及改写过程中选择Rust的原因、遇到的问题以及使用Rust改写的结果。

我们正在使用 Rust 开发的项目称为KCL。(KCL) 是一种开源的基于约束的记录和功能语言。KCL通过成熟的编程语言技术和实践改进了大量复杂配置的编写,致力于围绕配置构建更好的模块化、扩展性和稳定性,更简单的逻辑编写,快速的自动化和良好的生态扩展性。更多具体的KCL使用场景,请访问KCL官网。本博客不再赘述。

KCL之前是用Python写的。考虑到用户体验、性能和稳定性,我们决定用 Rust 重写它,并获得了以下好处:

  • 由于 Rust 强大的编译检查和错误处理,错误更少。
  • 语言端到端编译和执行性能提高 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。三种语言实现的堆栈虚拟机代码的细节在这里:https://github.com/Peefy/StackMachine。
  • 越来越多的编程语言的编译器或运行时,特别是前端的基础设施项目,都使用Rust编写或重构。此外,Rust已经出现在基础设施、数据库、搜索引擎、网络、云原生、UI、嵌入式等领域。至少,编程语言实施的可行性和稳定性已经得到了验证。
  • 考虑到后续项目开发会涉及区块链和智能合约方向,而社区中大量的区块链和智能合约项目都是由Rust编写的。
  • 通过Rust可以实现更好的性能和稳定性,使系统更容易维护,更稳定。同时,C语言的API可以通过FFI暴露出来,实现多语言的使用和扩展,有利于生态的扩展和整合。
  • Rust以一种友好的方式支持WASM。社区中大量的WASM生态系统是由Rust构建的。KCL语言和编译器可以在Rust的帮助下编译成WASM并在浏览器中运行。

基于以上原因,我们选择了Rust而不是Go。在整个改写过程中,我们发现Rust的综合素质确实很好(性能高,抽象度够)。虽然在某些语言特性上有一定的代价,尤其是寿命,但它的生态环境并不丰富。

使用Rust的困难是什么?
虽然我们决定用Rust重写整个KCL项目,但大多数团队成员都没有用Rust编写某个项目的经验,而我只学过The Rust Programming Language。我依稀记得,当我了解到Rc和RefCell等智能指针时,我放弃了。当时,我没有想到Rust中会有类似于C++的东西。

使用Rust的风险主要是Rust语言的学习成本,这在各种Rust博客中确实有提到。因为KCL项目的整体架构没有太大变化,而且d一些模块的设计和代码的编写都针对Rust进行了优化,所以整个重写的过程是在边学边练的过程中进行的。刚开始用Rust写整个项目的时候,我们还是花了很多时间在知识查询、编译和调试上。但随着项目的推进,我们在使用Rust时遇到的经验困难主要是思维转换和开发效率。

心智转换
首先,Rust的语法和语义很好地吸收和整合了函数式编程中类型系统的相关概念,如抽象代数类型(ADT)。此外,Rust中没有与 "继承 "相关的概念。如果你不能很好地理解它,即使是其他语言中的普通结构定义,在Rust中也可能会花费很多时间。例如,下面的Python代码在Rust中可能是这样定义的

Python

from dataclasses import dataclass

class KCLObject:
    pass

@dataclass
class KCLIntObject(KCLObject):
    value: int

@dataclass
class KCLFloatObject(KCLObject):
    value: float

Rust

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

当然,更多的时间是在与Rust编译器本身的错误报告作斗争。Rust编译器经常会让开发者 "碰壁",比如说借用检查错误。尤其是KCL编译器,它的核心结构是抽象语法树(AST),它是一个递归和嵌套的树状结构。

在Rust中,有时很难考虑变量的可变性和借贷检查之间的关系,就像KCL编译器中定义的范围结构Scope,对于循环引用的场景,它用来显示需要注意的数据的相互依赖性,同时大量使用Rust中常用的智能指针结构,如Rc、RefCell和Weak。

/// A Scope maintains a set of objects and links to its containing
/// (parent) and contained (children) scopes. Objects may be inserted
/// and looked up by name. The zero value for Scope is a ready-to-use
/// empty scope.
#[derive(Clone, Debug)]
pub struct Scope {
   
/// The parent scope.
    pub parent: Option<Weak<RefCell<Scope>>>,
   
/// The child scope list.
    pub children: Vec<Rc<RefCell<Scope>>>,
   
/// The scope object mapping with its name.
    pub elems: IndexMap<String, Rc<RefCell<ScopeObject>>>,
   
/// The scope start position.
    pub start: Position,
   
/// The scope end position.
    pub end: Position,
   
/// The scope kind.
    pub kind: ScopeKind,
}

开发效率
Rust的开发效率可以说是 "先抑后扬"。在手写项目的初期,如果团队成员没有接触过函数式编程的概念和相关的编程习惯,开发速度会明显慢于Python、Go、Java等语言。但是,一旦他们熟悉了Rust标准库的常用方法和最佳实践,以及Rust编译器的常见错误修改,开发效率就会大大提升,可以原生写出高质量、安全、高效的代码。

例如,我曾经遇到过一个Rust寿命错误,如下图代码所示。经过长时间的排查,发现lifetime不匹配是由于忘记标注lifetime参数造成的。另外,Rust的lifetime与类型系统、范围、所有权、借贷检查等概念相耦合,导致理解成本高、复杂度大,报错信息往往不像类型错误那样明显。lifetime不匹配的错误报告信息有时略显不灵活,这可能导致故障排除的成本很高。当然,在对相关概念更加熟悉后,效率会有所提高。

struct Data<'a> {
    b: &'a u8,
}

// func2 omit lifecycle parameters, and func2 does not.
// The lifecycle of func2 will be deduced as '_ by the Rust compiler by default,
// which may lead to lifetime mismatch error.
impl<'a> Data<'a> {
    fn func1(&self) -> Data<'a> {Data { b: &0 }}
    fn func2(&self) -> Data {Data { b: &0 }}
}

使用Rust重写收入比例
在团队的几个成员花了几个月的时间使用Rust完全重写并稳定地投入生产环境几个月后,我们回顾了整个过程,觉得收获很大。

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

虽然不是所有的模块都能获得40倍的性能(因为有些模块的性能瓶颈,如KCL运行时,是内存深拷贝操作),但我个人认为还是值得的。而当Rust使用了一段时间后,思维和开发效率就不再是限制性因素了。

结语
我个人认为,使用Rust重写项目后,最重要的是我是否学会了一种新的编程语言,或者说Rust是否很流行,我们使用Rust写了很多花哨的代码。这确实让KCL语言和编译器更加稳定,启动速度和自动化效率也不再是困扰了。KCL的性能比社会上同类型领域的其他编程语言要好,这样我们的语言和工具的使用者就可以体验到它的进步。这些都是由于Rust的无GC、高性能、更好的错误处理、内存管理、零抽象和其他特性。简而言之,作为用户,他们是最大的受益者。

最后,如果你喜欢KCL项目,或者想在自己的应用场景中使用KCL,或者想使用Rust参与开源项目,欢迎访问https://github.com/KusionStack/community,加入我们的社区,参与讨论和共建。