Lessons about Being Efficient from Rust

Yosi Pramajaya
4 min readFeb 20, 2020

In the beginning of 2020, I decided to learn Rust. After almost 2 months of intensive learning, I finally finished “the book”, done rustlings course, and I think I grasp at least the basic principles of Rust.

For me, learning Rust is not just learning another language. It feels like learning a new way of programming. Rust boasts on empowering everyone to build efficient and reliable software.

What do I learn about being efficient from Rust?

Zero-cost Abstraction Principle

What is zero-cost abstraction?

What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
- Stroustrup

One interesting way to do this is by removing Garbage Collector (GC). Almost all high-level programming languages use Garbage Collector. Of course it’s very important or else we have to manage memory allocation by ourselves, which is going to be horrible work. But even a very small garbage collector can still have impact on software performance.

How’s it possible for Rust to be able to work fast and efficient without GC? There are two main features in Rust that I want to cover here: Ownership and Lifetimes.

Ownership and Reference

The idea behind ownership is that there should only be one owner (variable) responsible for the value in the memory. How is this efficient? One reason is that it prevents duplication of values in the memory. Another reason is because Rust knows when the owner is out-of-scope, and when that happen the value can be freed from the memory.

Let’s see ownership in action. If you assign that variable to another variable, the ownership will be moved:

fn main() {
let x = String::from("Hello World");
let y = x;
println!("{}", x); //Won't compile as x is moved to y.
}

This also apply to assigning variable as parameter in function:

fn main() {
let x = String::from("Hello World");
print_z(x); //Ownership moved here
println!("{}", x); //Won't compile
}
fn print_z(z: String) { //Now z is the owner
println!("{}", x);
} // variable z (the owner) is out-of-scope, memory is freed here

But what if we really need to pass that value to a function? Instead of copying the value, we can allow the function to borrow the value, so that it can read the value and do it’s job. This is called “Reference”, with & sign. Like this:

fn main() {
let x = String::from("Hello World");
print_z(&x); //Ownership still
println!("{} from Main", x); //Now it works
}
fn print_z(z: &String) { //Just borrow the value to read
println!("{} from Function", z);
}

In the memory, there’s only one value allocated. There’re 2 types of References: Immutable and Mutable. But I think it’d be better to cover them when we discuss about Thread-safety in Rust.

Ownership is a very important concept for Rust, just like Object-Is-Everything concept for Java. The concept is not too difficult to understand, but maybe tricky to put it into practice. So, if you’re learning Rust now, it’d be great if you really understand the concept before move to deeper topics.

Lifetime of a Reference

The concept of lifetime is not really strange. In other language such as Java, when a variable is out of scope we cannot use it. But Rust’s concept of lifetime is somewhat different, it’s unique. How does it work?

Basically, GC works by looking for variables in memory that’s no longer used / out-of-scope, and free them. Rust doesn’t need GC because Rust compiler always check the lifetime of a variable / reference. This is called “The Borrow Checker”. If we try to use “dead” reference, it won’t compile.

fn main() {
let y = {
let x = String::from("Hello World");
&x
}; // variable x is freed here, it doesn't live long enough
println!("{}", y); //Won't compile
}

Most of the time, lifetimes are implicit and inferred. But sometimes, we may need to explicitly declare the lifetime. For example, take a look at this code:

// From Listing 10-21 in "The Rust Programming Language" book
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

The code above looks efficient. We only borrow 2 references, and return a reference of one of them. Therefore, no additional memory cost. But why the code above doesn’t work?

We don’t know the concrete lifetimes of the references that will be passed in, so we can’t determine whether the reference we return will always be valid. Rust compiler also can’t determine this, and therefore we need to explicitly declare the lifetime.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

In the code above, we explicitly tell the compiler that the reference that we return will have the same valid lifetime as x and y. I like what the book says about lifetime syntax:

Ultimately, lifetime syntax is about connecting the lifetimes of various parameters and return values of functions. Once they’re connected, Rust has enough information to allow memory-safe operations and disallow operations that would create dangling pointers or otherwise violate memory safety.

Thinking in Rust

From Rust, I learn that safety and efficiency cannot be separated. Just being fast is not enough if there’re so many unsafe code. In Rust, ownership and references not only helping us in memory management but also prevent data races in multithreaded environment. Going forward, even though I haven’t use Rust in production, learning about safety and efficiency from Rust can be helpful in building software in other language / platform.

I don’t know whether Rust will be the next standard of system programming. Some big tech companies have given it a try, hopefully it really will be the language for the next 40 years. Let’s see how it goes.

*Most of the information and sample code are taken from The Rust Programming Language Book.

--

--

Yosi Pramajaya

Tech Lead, Cloud Architect, Machine Learning Practicioner