A few days ago I found an interesting tweet: the posted code runs faster on Java, rather than on Go.
Quite interesting, right? So I asked ChatGPT to convert the code to Rust and see if it performs any better:
use std::time::Instant;
fn main() {
let timer = Instant::now();
for seed in 1..=1_000_000_000 {
let collatz_res = collatz(seed as u64);
if seed % 1_000_000 == 0 {
println!("Seed: {} Steps: {}", seed, collatz_res);
}
}
let elapsed = timer.elapsed();
println!("Took: {:?}", elapsed);
}
fn collatz(mut seed: u64) -> u32 {
let mut steps = 0;
while seed > 1 {
while seed % 2 == 0 {
steps += 1;
seed /= 2;
}
if seed > 1 {
steps += 1;
seed = seed * 3 + 1;
}
}
steps
}
It doesn’t. The code took 4.7 min on release mode, and a whopping 9.93 min on debug mode. Just like any developer thought, it probably performs a little bit better if we skip the debugging codes. Except, it didn’t. The code didn’t perform any better. It ran more or less similar to the one that had the debugging code added. So I left it for a while since I was in the middle of work, waiting for the build pipeline to finish.
After a while, the OP posted the root cause of why the Java code “runs” faster than Go code.
Right when I read the tweet, it clicked for me: I dismissed the Rust overflow error, and proceeded to update the data type from u32 (equivalent to int in Java) to u64 (equivalent to long in Java) just so that it runs without error. This is the reason why the Java code runs faster, because it overflows when you do seed = seed * 3 + 1 😂. @junderwood4649‘s tweet explains it better.
The Moral of the Story
Small Important Details
This case reminds me to try to be more careful on migrating (or “translating”) from old codebase to the new one, especially when the new codebase uses other language or tech. Small but important details, especially language-specific behaviors and how it will affect the new codebase should be thoroughly considered.
Everyone’s migrating from one language to another to efficiently scale their fleet or simply to have their service and apps run safer (examples: 1, 2). So this kind of “lost in translation” will happen more often, especially when you go to much stricter, safer language such as Rust.
Seasoned Devs Think Alike
On the first post, there’s a lot of developers who think the same: try to remove the debugging codes (reference: 1, 2, and 3) or maybe the JVM does some magic-magic that the Go and Rust don’t (I don’t understand this one as well). That means if you ask other developers something, you must tell them what you’ve tried and done.
Safer Language Will Save You HOURS of Debugging
Take a look at this tweet:
Because we now understand that the problem is the math is done in an incorrect variable type, you instantly notice that C runs faster too, because it was done in a int. Since there are no errors from C to tell that the calculation causes overflow, the function runs incorrectly, as confirmed by @jauhararifin10 themselves.
Imagine if this happens in much larger-scale codebases. If you didn’t catch this during rewriting it, then you’ll catch it on production :).
