We Rewrote Our Project With Rust… and It’s Almost 40X Faster
Here’s why we chose Rust, the roadblocks we encountered, and the rewrite revenue ratio
Rust has quietly become one of the most popular programming languages. As an emerging system language, Rust has many characteristics, such as the memory security mechanism, performance advantages close to C/C++, excellent developer community, toolchains, and IDEs.
This article will introduce the process we used to rewrite a project with Rust and gradually implement it in the production environment — along with the reasons for choosing Rust, the problems encountered, and the results.
The project we are developing using Rust is called KCL. (KCL) is an open-source constraint-based record and functional language. It improves the writing of complex configurations through a mature programming language stack. It is committed to building better modularity, scalability, and stability around configuration, simpler logic writing, fast automation, and good ecological extensionality. For more specific KCL usage scenarios, please visit the KCL website.
KCL was written in Python before. Considering the user experience, performance, and stability, we have decided to rewrite it in Rust, and the following benefits were obtained:
- Fewer bugs due to Rust's powerful compilation check and error handling.
- 66% improvement in language end-to-end compilation and execution performance.
- The performance of the language front-end parser has been improved by 20 times.
- The performance of the language semantic analyzer has been improved by 40 times.
- The average memory usage of the language compiler during compilation is half of the original Python version.
The Problems We Encountered
The compiler, build system, and runtime uses Rust to do similar things in projects of the same type — like deno, swc, turbopack, rustc. We used Rust to build the compiler's front, middle, and runtime—but we didn’t do this until about a year ago.
A year ago, we used Python to build the whole implementation of the KCL compiler. Although it ran well initially, as Python was easy to use, the team's research and development efficiency were also very high. However, with the expansion of the code and the increase in the number of engineers, code maintenance became more difficult.
We were forced to write Python-type annotations in the project and adopted stricter lint tools — and the coverage of code test lines has also reached more than 90% — but there are still many runtime errors, such as Python None empty objects, attributes not found, and so on. We must be careful when refactoring Python code, which seriously affects the user experience.
In addition, when KCL users constitute the majority of developers, any errors in the programming language or compiler's internal implementation become intolerable, leading to problems that affect our user experience. Programs written in Python have a slow start, and their performance needs to meet the efficiency demands of the online compilation and execution required by our automation system. In our scenario, users need to be able to quickly display compilation results after modifying the KCL code. A compiler written in Python cannot fulfill these requirements effectively.
Why Rust?
We chose Rust for the following reasons:
- We used Python, Go, and Rust to implement a simple programming language stack virtual machine and compared their performance. In this scenario, Go and Rust have similar performance, while Python showed a significant performance gap. After careful consideration, we chose Rust. The details of the stack virtual machine code implemented by the three languages are here: https://github.com/Peefy/StackMachine.
- More and more compilers or runtimes of programming languages, especially frontend infrastructure projects, are written or refactored using Rust. In addition, Rust has appeared in infrastructures, databases, search engines, networks, cloud-native, UI, embedded, and other fields. At least, the feasibility and stability of implementing programming languages have been verified.
- Considering that the subsequent project development will involve the direction of blockchain and smart contract, and a large number of blockchain and smart contract projects in the community are written by Rust.
- Better performance and stability can be achieved through Rust, making the system easier to maintain and more robust. At the same time, C APIs can be exposed through FFI for multilingual use and expansion, facilitating ecological expansion and integration.
- Rust supports WASM in a friendly way. Rust builds a large number of WASM ecosystems in the community. KCL languages and compilers can be compiled into WASM with the help of Rust and run in browsers.
Based on the above reasons, we chose Rust instead of Go. In the whole rewriting process, we found that Rust's comprehensive quality is really excellent (high performance and enough abstraction). Although there is some cost in some language features, especially the lifetime, it could be richer in ecology.
The Difficulties In Using Rust
Although we decided to rewrite the entire KCL project with Rust, most team members have no experience in writing a certain project with Rust, and I had only learned The Rust Programming Language. I vaguely remember that I gave up when I learned about intelligent pointers such as Rc
and RefCell
. At that time, I didn't expect that there would be anything similar to C++ in Rust.
The risk of using Rust is mainly the cost of language learning, which is indeed mentioned in various blogs. Because the overall architecture of the KCL project had not changed much, and some module design and code writing had been optimized for Rust, the entire rewrite was carried out in the process of learning while practicing.
When we first started using Rust to write the whole project, we spent a lot of time on knowledge query, compilation, and debugging. However, as the project progressed, the difficulties we encountered in our experience when using Rust were mainly mental transformation and development efficiency.
Mental Transformation
First of all, the syntax and semantics of Rust well absorb and integrate the concepts related to the type system in functional programs, such as the Abstract Algebraic Type (ADT).
In addition, there is no concept related to "inheritance" in Rust. If you need help understanding it, even ordinary structure definitions may take a lot of time in Rust than in other languages. For example, the following Python code will be defined differently in 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),
}
Of course, more time is spent fighting against the error reports of the Rust compiler itself. The Rust compiler will often cause developers to “run into a wall” when borrowing check errors, for example. Especially for the KCL compiler, its core structure is the Abstract Syntax Tree (AST), a recursive and nested tree structure.
It is sometimes difficult to consider the relationship between variable variability and borrowing check in Rust, Just like the scope structure Scope
defined in the KCL compiler, for scenarios with circular references, it is used to display the interdependence of data that needs to be aware of while making extensive use of intelligent pointer structures commonly used in Rust, such as Rc
, RefCell
and 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,
}
Development Efficiency
The development efficiency of Rust can be described as “restraining first and then improving.” At the beginning of a handwritten project, if team members have not been exposed to functional programming and related programming habits, the development speed will be significantly slower than that of Python, Go, Java, and other languages.
However, once they become familiar with the common methods and best practices of the Rust standard library and the common error modifications of the Rust compiler, the development efficiency will greatly improve, and they can write high-quality, safe, and efficient code natively.
For example, I encountered a Rust lifetime error, as shown in the following code. After troubleshooting for a long time, I found that the lifetime mismatch was caused by forgetting to label lifetime parameters.
Additionally, Rust’s lifetime is coupled with concepts such as the type system, scope, ownership, and borrowing check, resulting in a high cost and complexity of understanding. Error reporting information is often not as obvious as type errors.
The lifetime mismatch error reporting information is sometimes inflexible, which may lead to a high cost of troubleshooting. Of course, efficiency will improve after becoming more familiar with relevant concepts.
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 }}
}
Rewrite Revenue Ratio Using Rust
After several team members spent months using Rust to completely rewrite and stably put it into the production environment for several months, we reviewed the whole process and felt it was very rewarding.
From a technical perspective, the rewrite process trained us to learn a new programming language and programming knowledge quickly and put them into practice. The whole rewrite process made us reflect on the unreasonable design of the KCL compiler and modify it. For a programming language, this is a long-cycle project. We learned that the compiler system is more stable and safe, with clear code, fewer bugs, and better performance.
Although not all modules get 40 times the performance (because the performance bottleneck of some modules, such as the KCL runtime, is the memory-deep copy operation), I think it is still worthwhile. And when Rust has been used for a certain period, mind and development efficiency are no longer limiting factors.
Conclusion
The most important outcome of rewriting the project using Rust is not simply that I have learned a new programming language or that Rust is a very popular language that allows us to write fancy code. Rather, using Rust has made the KCL language and compiler more stable, eliminated startup speed and automation efficiency issues, and improved the performance of KCL over other programming languages in similar fields. These benefits are all due to Rust’s no-GC, high performance, better error handling, memory management, zero abstraction, and other features. In short, users are the primary beneficiaries.
Finally, if you like the KCL project, want to use KCL for your own scenarios, or want to use Rust to participate in an open-source project, you are welcome to visit https://github.com/KusionStack/community to join our community to participate in discussion and co-construction.