Trying out Julia
Julia is supposed to be as easy to write as Python and run as fast as a compiled language like C.
I’m a math nerd, so I tried this out by writing primality tests.
We check whether a number is prime by checking every possible odd factor up to the square root of the number.1
In Python this is:
def is_prime(number):
if number < 2:
return False
elif number < 4:
return True
elif number % 2 == 0:
return False
else:
root = int(number ** 0.5) + 1
divisor = 3
while divisor < root and number % divisor != 0:
divisor += 2
return number % divisor != 0
And we can call this from the Python REPL via
from slow_prime import is_prime
import time
x=time.time(); is_prime(72057594037928017); time.time()-x
Python took 8.07 seconds (average of 3 runs) to decide that 72057594037928017 was prime.
I chose 72057594037928017 = 2^56+81 (the first prime after 2^56) as my benchmarking prime.
The same function in Rust (like C, but strictly better) is:
fn is_prime(number:u64) -> bool {
match number {
0..=1 => false,
2..=3 => true,
_ => {
if number % 2 == 0 {
false
} else {
let root = ((number as f64).sqrt() as u64) + 1;
let mut divisor = 3;
while divisor < root && number % divisor != 0 {
divisor += 2;
}
number % divisor != 0
}
}
}
}
Rust doesn’t really have a REPL2, so I had to write some boilerplate code to get the number from the user and time it:
use std::io;
use std::time::Instant;
fn main() {
println!("Please input a nonnegative integer");
let mut number = String::new();
io::stdin()
.read_line(&mut number)
.expect("Failed to read line");
let number: u64 = number.trim().parse().expect("Not a nonnegative integer");
let now = Instant::now();
println!("{}",is_prime(number));
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
}
Rust took 0.848 seconds (average of 3).
Julia syntax is about as easy to write as Python:
function is_prime(number)
if number < 2
false
elseif number < 4
true
elseif number % 2 == 0
false
else
root = floor(Int,√number)+1
divisor = 3
while divisor < root && number % divisor != 0
divisor +=2
end
number % divisor != 0
end
end
And in Julia you can calculate the square root via √number
instead of the less intuitive number ** 0.5
.
The Julia REPL is also easy to use:
include("prime.jl")
@time is_prime(convert(UInt64,72057594037928017))
Julia took 0.903 seconds (average of 3).
(I can also just run is_prime(72057594037928017)
, which is about 5ms slower.)
1. Closing thoughts
Rust nudged me toward the optimization of using unsigned integers instead of signed integers.
I’ve read an argument that small benchmarks like this are silly, since Julia’s real speed advantage is that it’s still fast when different libraries have to work together, which is always slow in Python.
I’ve also heard people say that Julia’s worst flaw is that its libraries are buggy and don’t work well together. I will keep using Julia and see if I experience this.
Speaking as a hobbyist, I think Julia’s speed and easy syntax are appealing.
But speaking as an engineer, I think reliability is king—Rust wins that. And my impression is that Python has the biggest ecosystem, so I can just “import X” instead of “write code to do X”.
The base Julia language seems on balance better than Python. But there’s a Catch-22: people won’t start using Julia until it has lots of reliable libraries, which won’t happen until lots of people start using Julia.
Footnotes:
Plus some special cases at the beginning. Of course there are much more efficient algorithms than this one, which is why I called my module slow_prime
.
There are Rust REPLs, like Papyrus, which I have not tried. I get the impression (possibly wrong) that these are cool projects, but not the way that the typical Rustacean writes Rust.