Rust lang Tips and Tricks

Rust is a programming language that is geared towards speed and safety. Rust has gained a lot of adoption in the past few years and developers are loving it. Although there are numerous other systems programming languages, Rust is one of the very few alternatives to C/C++ that can hold its own when it comes to performance. 

Rust programming language
Source

Rust is the language of our choice at Polymath for our upcoming Blockchain, Polymesh. In this post, I’ll be sharing some of my favorite Rust tricks and some tips for the new developers.

Enum size is bounded by the largest member

Enums are sized to be able to hold their largest variant. Therefore, it’s recommended to have similar sized variants within enums to avoid having a nonoptimal memory layout. You can consider boxing the larger variants if required. Consider this example:

enum Foo{
A(u64),
B([u64; 1000]),
}

enum FooBoxing {
A(u64),
B(Box<[u64;1000]>)
}

fn main() {
let x = Foo::A(0); // Its size is 8008 bytes.
let y = FooBoxing::A(0); // Its size is just 16 bytes.
println!("Foo size {:?}", std::mem::size_of_val(&x));
println!("FooBoxing size {:?}", std::mem::size_of_val(&y));
}

In the above example, variant A of enum Foo is much smaller in size than variant B but the memory layout used for both variants will be the same and hence when variant A is used, it will have nonoptimal performance.

Avoid unnecessary clones

Calling .clone() on a variable creates a copy of their data and it takes resources to create a copy. Therefore, clones have a negative performance effect in most cases and should be avoided. Often times, you will be able to pass references of the same variables to different functions rather than needing to clone them. For example:

fn main() {
let x = Foo::new();
func(x.clone());
func(x.clone()); //This clone is not needed
}

fn main() {
let x = Foo::new();
func(x.clone());
func(x); // This will work fine because you do not need
// to use x after this call
}

fn main() {
let x = Foo::new();
// If you are able to edit func, it's likely that
// you will be able to modify it to use references.
// You wont need to use any clones then.
func(&x);
func(&x);
}

Modularizing tests

If you structure your tests like

tests/   
foo.rs
bar.rs

Then each one of those tests gets compiled as a separate binary, and that takes more compilation time and space. You can instead add your test files as modules to a single test so that only a single binary is generated. Your new tests structure will look something like:

tests/
all/
mod.rs // mod foo; mod bar;
foo.rs
bar.rs
mod.rs // mod all;

I was able to cut our CI test time in Polymesh from 26 minutes to 8 minutes using this trick. It has the disadvantage that you can not change one test file and compile just that file. It will always compile the full binary even if you have changed just one test file. For us, compiling individual files was only ~10 seconds faster than compiling the full binary and hence we decided to go with this approach.

The dbg! macro

The dbg macro can be used to print the value as well as the source code of an express to stderr. Example usage:

let a = 2;
let b = dbg!(a * 2) + 1;

The above code will print:

[src/main.rs:2] a * 2 = 4

Using _ to make large numbers legible

In Rust, you can use _ between numbers to make them easier to understand.

let answer = 42000000; // ugly
let answer = 42_000_000; // beautiful

The standard swap function

The swap function allows you to directly swap two variables without needing to create a temporary variable.

use std::mem;

let mut x = 5;
let mut y = 42;

mem::swap(&mut x, &mut y);

assert_eq!(42, x);
assert_eq!(5, y);

? converts between error types

As you probably already know, ? can be imagined as unwrap that returns the error instead of panicking. Instead of directly returning the error, ? actually returns Err(From::from(err)). That means the error is automatically converted to the proper type if it is convertible.

Same name macro, function, and type

It is possible to declare a macro, a function and a type like an Enum with the same name and then import all three of them elsewhere with a single import statement.

sccache

sccache can cache cargo build artifacts so that they can be reused across workspaces. This means that if you have multiple Rust projects and they use an identical dependency, sccache will allow you to compile that dependency once and then reuse across the projects. It will save you compilation time and disk space. Rust target dirs are already so big, there’s no reason to store redundant binaries there.

rust target directory is very heavy

You can install sccache with Cargo: cargo install sccache. To enable sccache, you need to add RUSTC_WRAPPER=sccache to your build environment. One way to do that is to add export RUSTC_WRAPPER=sccache to your .bashrc.

Clippy and rustfmt

They are two of my favorite Rust tools and if you don’t already use them, give them a try. Clippy can catch a variety of lints in your code and helps you write idiomatic code. To install Clippy, run rustup component add clippy and to run Clippy in your workspace, execute cargo clippy. More information can be found on Clippy’s GitHub.

rustfmt, as the name suggests, is a tool for formatting Rust code according to style guidelines. To install rustfmt, run rustup component add rustfmt and to run rustfmt in your workspace, execute cargo fmt. More information can be found on rustfmt’s GitHub.

I’d also like to give a shoutout to rust-analyzer. It is an experimental modular compiler frontend for the Rust language. It works better than any other Rust compiler frontend for me. I highly recommend everyone to try it out.

Conclusion

Rust has a lot to offer and you can learn something new about Rust every day. I hope you learned something new from this post. If you want to add something or need help, feel free to leave a comment or reach out to me via other mediums.

Happy Hacking!

4 thoughts on “Rust lang Tips and Tricks”

  1. Hey!
    Great article. I actually did learn a few things. Please continue 🙂

    Any tips on learning to live with that ‘crabby’ compiler 😉 would be welcome. Especially how to find solutions when the error massage does not point to a solution. Or specific solutions to common cryptic error messages.

    1. I think the most common types of issues that beginners struggle with are lifetime and borrow checker related. Cloning variables can solve a lot of those issues. With time, you’ll be able to better understand what’s going on and fix it properly.

      Another common issue I found on our CI was that when the rust compiler was running out of RAM, it was failing to compile but not showing any memory-related errors. Using `-j 1` flag in cargo set the maximum threads to 1 and lowered the memory usage to what’s available on our CI containers.

      I also find some Macro errors to be quite frustrating as often times they do not point you to the exact line but the whole macro. More often than not, it’s just a syntax error.

  2. I do see why having imbalanced enum members is unwieldy but am not sure what makes them suboptimal, performance wise. Isn’t using Box to allocate on the heap going to be more taxing in comparison to it’s stack counterpart, imbalanced or not? Or do you mean, more in the sense that is generally not wise to put large structures in the stack?

    1. The performance penalty I am talking about is for smaller variants that don’t need to be boxed. Referring back to the example in the original post, you need to process 8008 bytes vs 16 bytes when accessing variant `A`. In both cases, variant `A` is directly stored on the stack.

      If both variant `A` and `B` are large then you have a balanced Enum and this performance penalty does not imply. You then need to think if it’s worth putting this large data on the stack or is it better to add redirection.

Leave a Comment

Your email address will not be published. Required fields are marked *