Few days ago I found an interesting tweet: the posted code runs faster on Java, rather than Go.
Quite interesting, right? So I asked ChatGPT to convert the code to Rust and see if it perform 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 doesn’t. The code doesn’t perform any better. It runs more-less as similar as the one with the debugging code. So I leave 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 just clicked on me: I dismissed the Rust overflow error, and proceed to update the data type from u32
(equivalent to integer
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 code base to the new one, especially when the new code base uses other language or tech. Small but important details, especially language-specific behaviors and how it will affect your new code base 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 runs 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 that thinks the same: try to remove the debugging codes (reference: 1, 2, 3) or maybe the JVM do some magic-magic that the Go and Rust don’t (I don’t understand this one as well). That means if you ask something to other developers, you must tell them what have you tried and done to the code.
Safer Language Will Save You HOURS of Debugging
Take a look on 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
them self.
Imagine if this happens in much larger scale codes. If you didn’t catch this during rewriting it, then you’ll catch it on production :).