DEV Community

Cover image for Rust is SLOW actually? - AOC 2023 day 1 performance comparison

Rust is SLOW actually? - AOC 2023 day 1 performance comparison

Jacob W Runge on December 01, 2023

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 fas...
Collapse
 
manchicken profile image
Mike Stemle

You’re doing a lot more type conversion in Rust than you need to, too. You’re also not checking your inputs in JavaScript like you are in Rust.

I think the biggest thing, though, is that you’re recreating the HashMap<String, &str> for the words with every call of check_for_words(), but you’re using a global in the Node program. Since you’re calling the str.to_string() it isn’t optimizing it out in the compiler.

Collapse
 
jwrunge profile image
Jacob W Runge

Oh man, good catch. I bet that has a pretty significant impact. I'll give it a go!

I think the check-for_words func is going to end up being unnecessary, and I'll just end up grabbing the first number I see whether a word or a digit. I'll have to update the js for that to be a fair comparison, though! Either way, you're right... that hashmap needs re-scoped.

Collapse
 
viiik profile image
Eduard

First thing I see in the rust code is this:

fn first_or_last_number(input: &str, reverse: bool) -> Option<char> {
    let new_str = match reverse {
        true => input.chars().rev().collect::<String>(),
        false => input.to_string()
    };

    println!("New string: {}", new_str);

    for c in new_str.chars() {
        if c.is_digit(10) {
            return Some(c);
        }
    }

    None
}
Enter fullscreen mode Exit fullscreen mode

There is no need to actually .collect the reversed string, if the last character was a digit, you are still building a possibly thousands of characters long string just to ignore all of them.

You should try using the iterators directly, make the function take a character iterator, then for the last digit just send a reversed iterator.

Collapse
 
jwrunge profile image
Jacob W Runge

OK, trying this out now. It makes perfect sense that it is more efficient to use the iterators, but I'm running into the problem of mismatched types (which is why I originally wrote the code like this): input.chars() returns a Chars iterator, but input.chars().rev() returns a Rev<Chars>> iterator.

So it seems like the tradeoff is duplicating code (if reverse, for c in input.chars() and again else for c in input.chars().rev()) to get optimal performance. That's what I'll go with, but is there no way to make it a little DRYer? (I'm guessing the answer is a custom macro? Not sure I'm ready to tackle that!)

Collapse
 
viiik profile image
Eduard

I believe you could make your function generic over a char iterator, which Rev implements: doc.rust-lang.org/std/iter/struct....

Thread Thread
 
jwrunge profile image
Jacob W Runge

Ah, thanks! I couldn't find it for some reason.

Collapse
 
jwrunge profile image
Jacob W Runge

Thanks! I'm going back through this code tonight, so I'll give this a go.

Collapse
 
artxe2 profile image
Yeom suyun

No matter how optimized the code is, if Rust was much faster than JS in a single execution, the noteworthy aspect is likely that JS performed exceptionally well in repeated executions.
This phenomenon could be attributed to hot code optimization and garbage collection.
Though not an expert, I believe JS has features to optimize frequently executed functions, and if GC doesn't run, continuously allocating memory might result in faster measurements than actual performance.

Collapse
 
jwrunge profile image
Jacob W Runge • Edited

Thanks, Yeom! This was really at the heart of why I was so confused. Node or JS itself has to be doing something like this to achieve the result it does. That also explains (at least partially) why the JS code is so much faster for 1000x the load than a single execution.

EDIT: The commenter above pointing out the HashMap being re-created each loop has probably identified the key issue.

Still really curious why the Rust code doesn't scale linearly. It must be doing something similar, but without relying on the garbage collector?

Collapse
 
valeriavg profile image
Valeria • Edited

Interesting 🧐 The first thing that comes into my mind is that Rust shines in cases where garbage collector creates a bottleneck: for example when you store a large amount of data in memory and change it rapidly. In this case you probably have one constant variable and no garbage to collect.

JavaScript indeed has a “cache” for operations: I’ve seen similar results with WASM vs JavaScript comparison.

Once again, it doesn’t mean that JavaScript is faster than Rust, it simply means that in this specific scenario one tool is suited better than the other.

Some years ago I’ve read an article on switching from Go to Rust by Discord and I think this is a great use case where Rust would outperform anything else.

Collapse
 
jwrunge profile image
Jacob W Runge

Cool -- I never knew about JS's operations cache. I'll need to look into that more!

A comment above pointed out that I was using a global map in the JS, but recreating the map each time in Rust. I'm updating the code and re-running benchmarks, so we'll see how it shakes out! I'll update the post with results.

Collapse
 
jwrunge profile image
Jacob W Runge

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.

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.

Collapse
 
manchicken profile image
Mike Stemle

It’s marvelous to watch you dig in here and challenge yourself to understand with the help of the community. Solidarity.

Collapse
 
jwrunge profile image
Jacob W Runge

Thanks so much! Appreciate the help, and your time!