Two commutes with Rust

Over the last couple of commutes to and from work I’ve been playing with Rust, which went v1.0 over the weekend.

Rust is touted as a systems language in the same vein as C, C++ and to a lesser extent, Google’s Go. It offers both high level abstractions like generic programming and object-orientism while also giving access to lower-level facilities like fine-grained memory allocation control and even inline assembly.

Critically for me, it has a “only pay for what you use” mentality like C++, a well-sized standard library and runtime, and no garbage collection. It’s quite feasible to use Rust to make a “bare metal” system in (for example, Zinc).

One of the novel things Rust brings to the table is its memory ownership semantics. Each allocation’s lifetime is tracked by the compiler (with an occasional helping hand from the programmer). Passing references to objects invokes the “Borrow Checker” which makes sure nobody holds on to objects beyond their lifetime. This solves a lot of memory ownership issues (maybe all of them?) up front, in the compiler. I love this.

Another nice feature is having proper, deterministic “destructors” that run when a variable binding goes out of scope. I miss this a lot in those times where I’m programming in a language other than C++.

So I loved the sound of all this, but in order to see how it all fitted together in practice, I decided to port a C++ path tracer to Rust, and see how I’d get on. I’m fond of smallpt, a 99-line C++ program that generates lovely images. While I was still at Google, I did the same experiment with the (then unreleased) Go language; and found the code generator lacking, and the development experience a little lacking. How would Rust fare?

Pretty well!

First steps

Firstly I hacked up a Vec3d class to do all the 3D maths needed. It was surprisingly easy to make a struct, and then add an impl to do all the operations I needed. As a bonus, by impl-ing the relevant Add, Sub etc traits, I was able to get things like let a = b + c; working for vectors.

pub struct Vec3d {
    pub x: f64,
    pub y: f64,
    pub z: f64
}
impl Vec3d {
    pub fn dot(self, other: Vec3d) -> f64 {
        self.x * other.x + 
            self.y * other.y + 
            self.z * other.z
    }
    ...
}

impl Add for Vec3d {
    type Output = Vec3d;

    fn add(self, other: Vec3d) -> Vec3d {
        Vec3d { 
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z 
        }
    }
}

Source of the above here, full source on github.

Immediate things I found:

Making pictures

Once the Vec3d class was finished (and even had a simple test), I moved on to the body of the renderer. It was fairly simple to get up and running. Probably the trickiest part was remembering to type cargo build instead of make!

Things I learned getting the first image rendered:

This leads to nice code like:

let mut result : Option<HitRecord> = None;
for sphere in scene {
    // if let will assign to 'dist' if the return of intersect
    // matches "Some".
    if let Some(dist) = sphere.intersect(&ray) {
        // if we don't currently have a match, or if this hit
        // is nearer the camera than an existing result, then
        // update 'result' with this hit.
        if match result { None => true,
                          Some(ref x) => dist < x.dist) } {
            result = Some(HitRecord { 
                sphere: &sphere, dist: dist 
            });
        }
    }
}

An example:

struct HitRecord<'a> {
    sphere: &'a Sphere,
    dist: f64
}

fn intersect<'a>(scene: &'a [Sphere], ray: &Ray) 
    -> Option<HitRecord<'a>> {...}

Here the lifetime indicator 'a is used to show the sphere reference inside the HitRecord is only valid while the similarly-tagged scene slice is. The compiler will give an error if you try and let a HitRecord outlive the scene it came from.

With all that in place I got my first image:

A beautiful picture
A little example of path tracing in action.

Performance

If you look at the assembly output of Rust (e.g. with Rust explorer, you can see it’s able to utilize the LLVM backend to do some impressive SSE2 code generation. Coupled with the “no allocations unless you ask for them”, and no need to stop the world for garbage collection etc, it performs well.

In my simplistic benchmark on my laptop during my commute, it performs the same (within a second or so) as the smallpt.c compiled with -O3 -fopenmp, at least once I put in rudimentary threading to match the OpenMP implementation in smallpt. I’ll run a longer test (and debug some IO slowdowns that are contributing to the difference) and post again with more information.

All in all I’m extremely excited and I look forward to spending more time hacking on Rust!

UPDATE: I’ve now written a follow-up post on the performance numbers, having fixed a number of bugs in the Rust version.

Filed under: Coding Rust
Posted at 18:40:00 BST on 21st May 2015.

About Matt Godbolt

Matt Godbolt is a C++ developer living in Chicago. Follow him on Mastodon or Bluesky.