No side effects in Rust programs by default, and this is a good, good thing

As I mentioned here, I’m learning some Rust. I’m finding Programming Rust to be a nice guide, though I’m still in the early pages – I keep getting distracted by… , well, Rust. I read one thing and then go off and play with 3 other related things. I’m having so much fun and want to share a bit here – one of my experiments to start:

One of the first things I read about Rust is this

“By default, once a variable is initialized, its value cannot be changed”

I am in love. ❤️

I studied programming languages for three years at Indiana University in the early 1990s and it was there and then that I was introduced to functional programming. One of the things that characterizes functional programming is exactly the above – that by default you don’t program with side effects, though from a stylistic perspective Scheme (what we mostly used at IU) and Rust differ in a number of ways. For example, it seems that variable declarations play a larger role in Rust than in Scheme – we rarely used let and generally used define only for functions – and I think that is one of the reasons that the mechanism to go against the no-side-effects default is so notably different. In Rust, you declare your intent to side effect as a part of the variable declaration with the mut keyword: let x = 0; will not allow x to be changed later, but let mut y=0; will allow y to be reassigned. In Scheme the go-against-the-default syntax is at the point of the reassignment operation, and to make the programmer hyper-aware that they are doing something “unnatural” an exclamation point is a part of a keyword for the operation: When you declare a variable with something like (define x 0) you are not making any statement about whether you intend to change the value later, but if you do, you’ll use (set! x 2). I haven’t taken the time to think about whether I  like one programming model better than the other, nor have I analyzed whether the different approaches allow the compiler to do different types of analyses – maybe I’ll get to posting on that some other time.

But, stylistic differences aside, the point is that syntax tells the compiler when side effects should be expected, and because you generally don’t use them – and here’s the great part – when you stick with the no-side-effects default, it can do all sorts of optimizations on your behalf. Programming languages are basically mathematical models – and without side effects in a program, many more calculations can occur at compile time, with a variety of benefits. One of those might be performance, and that is where I am going with this story.

Okay, so I read the above sentence about immutability and then a few pages later find the first sample program that calculates the greatest common divisor (GCD) of two numbers doing what? It calculates it in a while loop using side effects! Gah!!!🤨

So I redid it:

fn gcd(mut n: u64, mut m: u64) -> u64 {
    assert!(n != 0 && m != 0);
    while m != 0 {
        if m < n {
            let t = m;
            m = n;
            n = t;
        }
        m = m % n;
    }
    n
}

I wrote tests to make sure both versions worked (to learn a language you have to learn how to write tests in that language) and then because of Rust’s reputation for the compiler doing so much for the programmer, I had a hunch that the version without side effects would run faster than the original. I worked up a little example that calculates the GCD of three very large numbers and invokes the calculation a million times so that I could test my hypothesis. And my hunch was right: the version that had no side effects generally took between 234ms and 242ms whereas the version with variable reassignments took between 252ms and 265ms (release built). Sure, it’s not a huge difference, and I haven’t stepped through any assembly code (and I won’t😉) to verify the difference, but I’m pretty confident I can thank the Rust compiler for the slight advantage.

And then I got curious – what would this look like in java? I had another hunch, this time that the recursive implementation that did not use side effects would go slower because there is generally more involved in making a function call than just changing a program counter somewhere, and I was pretty sure the Java complier wouldn’t be looking for optimizations like the Rust compiler does. So I coded up the same functions in java and ran it. Sure enough, the version that had no side effects was slower, generally taking between 354ms and 370ms, whereas the version with variable reassignments took between 330ms and 345ms.

Given these simple experiments, it seems the optimizations that the Rust compiler applies when side effects are not used make up for the difference between the work involved in making function calls and a simple change in a program counter – heck, maybe it even optimizes it to program counter change.

For those of you who might not know what the heck I am talking about with this program counter thing, don’t sweat it – just focus on the main punchline: the Rust compiler applies many optimizations for you, and can do more of them when you avoid changing the values of variables after they are declared. So stick with the default whenever you can – specifically avoid mut. Chances are if you’ve been programming in languages like java, javascript, c/c++, php, and many others, this will feel a bit unnatural to start, but it’s worth the effort to change your habits.

Finally, while in this post I’ve explored the difference in performance, it’s also worth noting that when you avoid side effects you also eliminate certain classes of errors from your code. It’s likely that in the long run this is even more valuable than the performance improvement I describe here, even if that benefit is a bit difficult to measure.

Note: I’ve cross-posted this blog both on my personal (WordPress) site and on Medium, because I’ve not yet decided which one will be my long term solution. Would be happy to have any advice on that)

One Comment

  1. Alex

    January 3, 2023

    Hi Cornelia,

    I’m currently learning Rust with the intention to apply it to Cloud Architecture and Programming Language Design (I’m a COMPLETE beginner). I also just started reading Cloud Native Patterns as well as my applied learning along with reading the Rust Book.

    Rust is awesome! I clicked on your link from the PDF version of the book and was happy to see that you are dabbling in Rust as well.

    I don’t use Medium personally, but I think that there’s no harm in cross-posting.

Share Your Thoughts