Rust is SLOW actually? - AOC 2023 day 1 performance comparison
Tags: rust, node, javascript, discuss
Published: 2023-12-01 21:12:55

UPDATE: I've stuck a comment below with updated benchmarks after the community helped me to improve my Rust code. Spoiler alert: Rust is wicked fast. WAY faster than the equivalent JavaScript. I've pasted that comment [here at the end of the post](#getting-the-rust-right), in case the comment gets buried someday.

---

I just got through with the [Advent of Code 2023 Day 1 challenge](https://adventofcode.com/2023/day/1), and [I wrote about my trials and tribulations](https://dev.to/jwrunge/calibrating-an-elven-trebuchet-in-rust-advent-of-code-2023-day-1-15kj) in great, long-winded detail. I'm a frontend guy who's used to working in TypeScript, a near-total Rust newb, so I thought it might be interesting to do a little comparison write-up. I ran through the challenge in Rust, then circled back and ported it to JavaScript (running in Node.js). After the lengthy account in my initial post, I figured the comparison would be short and sweet, just some general impressions about what I like about each language, with a fun little benchmark at the end that showed just how much faster Rust was.

Then I ran the benchmarks, and what I saw was shocking enough that I figured I'd better yank the benchmarks [out of my comparison article](https://dev.to/jwrunge/comparing-apples-and-oranges-rust-and-javascript-in-aoc-2023-day-1-3i29) and make a dedicated post in an attempt to get some answers.

Full disclosure: I'm not a benchmarker. When my apps run slow, I do performance testing, but beyond that, I'm usually happy just knowing something is "fast" or "slow." And in my mind, Rust is fast and JavaScript is slow.

But that's not what I'm seeing in these numbers, and I think I need a sanity check.

## My test

To test execution speed, I used PowerShell's `measure-command` (I use Windows, btw -- why are you laughing?). `measure-command` is a rough equivalent to the `time` command in Unix.

I compiled the Rust code using `cargo build --release`, then ran each implementation 5 times and took the average execution speed.

## The initial results

Running each, we end up with: JavaScript, avg 39.9254ms; Rust: 18.3171ms. Rust is faster by ~54%.

That makes sense. I don't doubt this result at all.

## Let's see if that scales

I then ran each implementation on on a loop (internal to the code, not via the command line) 1000 times and see how it handles prolonged activity. That's an `fs::read_to_string()` (Rust) or an `fs.readFileSync()` (JS) followed by processing 1000 strings, 1000 times.

This is where the results get a little sus. JavaScript: 3260.6001ms; Rust: 6564.07532ms. Oof... Rust is more than twice as *slow*. That doesn't feel right...

I thought perhaps that Rust's file reader implementation just wasn't up to snuff, assuming Node's `readFileSync` implements some battle-tested C or C++ function, but no... removing file reads seemed to have little affect on either implementation.

I'm not exactly dissatisfied with Rust's performance: it actually scaled pretty well, as far as I understand it. With 1000x the work, Both programs scaled well; with 1000x the work, the Rust code was only ~358x slower. The prolonged activity didn't scale linearly, instead finishing significantly faster that one would expect. This, to me, suggests one of two things:

* There is a hefty invocation cost (this doesn't seem likely - running the program once only cost ~18ms)
* There is a "warm-up" period, with the code executing faster over time

I suppose a third option could be that invocation is slow (say, 17ms); the code executes initially very fast (1ms), and then *slows down* over the duration of the program's runtime. That seems less likely to me.

## Is Node just really fast?

In any case, the JavaScript code was only ~82x slower at 1000x the load. So another possible explanation would be that the Node is doing some kind of magic like caching the file read or certain execution patterns... In which case, highly repetitive tasks could be a rare performance boon for scripting languages like JS.

It's also possible (likely) that, as a Rust newb, there is some kind of inefficiency in my code that is exacerbated by prolonged run time / repetition. That code, in addition to being mostly included in my previous article, can be found [on GitHub](https://github.com/jwrunge/advent_of_code_2023).

It's also possible that I'm a JavaScript prodigy and have the unique ability to make it run faster than compiled code. /s

So I'm curious, Rustaceans and Node... Node-heads? What could be the cause of this discrepancy between a single execution and execution over time?

---

## Getting the Rust right

UPDATE: I've got my answer. Rust is, in fact, WAY faster, ESPECIALLY in the 1000x test. Many thanks to all who commented and helped me get my Rust up to snuff. 😅

I updated my Rust code to use iterators directly (thanks Eduard!), globalize the HashMap instead of recreating it each function call (thanks Mike Stemle!), and generally be smarter about searching for numbers (not doing full string replaces, but stopping my loops as soon as a valid match was found). Where those changes could be ported to the JS version, I did so. [Full code on GitHub](https://github.com/jwrunge/advent_of_code_2023).

Here are the new results:

Rust single iteration is down to 10.161ms (almost cut in half from the previous 18.317!).

JavaScript single iteration is up to 71.723. It looks like the previous version was more efficient at ~39ms, which just goes to show you that porting code one-to-one between languages as different as Rust and JavaScript isn't always the best practice. While it would be interesting to investigate further, I think it's most fair to compare the fastest working JS code to the fastest working Rust code here, and Rust ends up at ~25% of the JS code's total the execution time.

Rust 1000x iterations is down to an *amazing* 385.875 ms, 17x faster than before (at ~6 seconds) and only ~38x slower for 1000x the work. 🤯

JavaScript 1000x iterations comes out to 3691.43. This is, not surprisingly, MUCH slower than the Rust 1000x test (by ~100x) and slower than the previous JS version's 1000x test. What IS surprising is that it's not off the previous version by much, with the old code coming in at 3260.6. So despite the difference in single-iteration performance of the JS code from old to new nearly doubling, the 1000x iteration test only ends up being ~400ms slower.

That said, in that 400ms difference, the Rust code could have run it's full 1000 iterations and had a spot of tea.