I read Minimal distance in Manhattan metric and rewrote the author's "naive" implementation in Rust. The C++ variant is:
#include <utility>
#include <cstdio>
#include <cstdlib>
std::pair<int, int> pointsA[1000001];
std::pair<int, int> pointsB[1000001];
int main() {
int n, t;
unsigned long long dist;
scanf("%d", &t);
while(t-->0) {
dist = 4000000000LL;
scanf("%d", &n);
for(int i = 0; i < n; i++) {
scanf("%d%d", &pointsA[i].first, &pointsA[i].second);
}
for(int i = 0; i < n; i++) {
scanf("%d%d", &pointsB[i].first, &pointsB[i].second);
}
for(int i = 0; i < n ;i++) {
for(int j = 0; j < n ; j++) {
if(abs(pointsA[i].first - pointsB[j].first) + abs(pointsA[i].second - pointsB[j].second) < dist)
dist = abs(pointsA[i].first - pointsB[j].first) + abs(pointsA[i].second - pointsB[j].second);
}
}
printf("%lld\n", dist);
}
}
The Rust variant is:
use std::io;
use std::io::BufReader;
use std::io::BufRead;
fn read_array(stdin: &mut BufReader<io::Stdin>, array_len: usize, points: &mut Vec<(i32, i32)>) {
let mut line = String::new();
for _ in 0..array_len {
line.clear();
stdin.read_line(&mut line).unwrap();
let mut item = line.split_whitespace();
let x = item.next().unwrap().parse().unwrap();
let y = item.next().unwrap().parse().unwrap();
points.push((x, y));
}
}
fn manhattan_dist(a: &(i32, i32), b: &(i32, i32)) -> u32 {
((a.0 - b.0).abs() + (a.1 - b.1).abs()) as u32
}
fn main() {
let mut line = String::new();
let mut stdin = BufReader::new(io::stdin());
stdin.read_line(&mut line).unwrap();
let n_iters = line.trim_right().parse::<usize>().unwrap();
let mut points_a = Vec::with_capacity(10000);
let mut points_b = Vec::with_capacity(10000);
for _ in 0..n_iters {
line.clear();
stdin.read_line(&mut line).unwrap();
let set_len = line.trim_right().parse::<usize>().unwrap();
points_a.clear();
points_b.clear();
read_array(&mut stdin, set_len, &mut points_a);
read_array(&mut stdin, set_len, &mut points_b);
let mut dist = u32::max_value();
for i in points_a.iter() {
for j in points_b.iter() {
dist = std::cmp::min(manhattan_dist(i, j), dist);
}
}
println!("{}", dist);
}
}
Then, I generated data with a Python script:
import random
ITER = 100
N = 10000
MAX_INT = 1000000
print("%d" % ITER)
for _ in range(0, ITER):
print("%d" % N)
for _ in range(0, N):
print(random.randrange(-MAX_INT, MAX_INT + 1), random.randrange(1, MAX_INT + 1))
for _ in range(0, N):
print(random.randrange(-MAX_INT, MAX_INT + 1), random.randrange(-MAX_INT, 0))
And compiled both variants with g++ -Ofast -march=native
and rustc -C opt-level=3
respectively. The timings are:
C++
real 0m7.789s
user 0m7.760s
sys 0m0.020s
Rust
real 0m28.589s
user 0m28.570s
sys 0m0.010s
Why is my Rust code four times slower than the C++ variant? I am using Rust 1.12.0-beta.1.
I added time measurements:
let now = SystemTime::now();
line.clear();
stdin.read_line(&mut line).unwrap();
let set_len = line.trim_right().parse::<usize>().unwrap();
points_a.clear();
points_b.clear();
read_array(&mut stdin, set_len, &mut points_a);
read_array(&mut stdin, set_len, &mut points_b);
io_time += now.elapsed().unwrap();
let now = SystemTime::now();
let mut dist = u32::max_value();
for i in points_a.iter() {
for j in points_b.iter() {
dist = std::cmp::min(manhattan_dist(i, j), dist);
}
}
calc_time += now.elapsed().unwrap();
And writeln!(&mut std::io::stderr(), "io_time: {}, calc_time: {}", io_time.as_secs(), calc_time.as_secs()).unwrap();
prints io_time: 0, calc_time: 27
.
I tried nightly rustc 1.13.0-nightly (e9bc1bac8 2016-08-24)
:
$ time ./test_rust < data.txt > test3_res
io_time: 0, calc_time: 19
real 0m19.592s
user 0m19.560s
sys 0m0.020s
$ time ./test1 < data.txt > test1_res
real 0m7.797s
user 0m7.780s
sys 0m0.010s
So it is at now ~ 2.7x difference on my Core i7.
That is because the languages are different and choose different tradeoffs. The tradeoffs might make a language inapplicable in your domain. Rust has always been marketed as a "systems programming language". C++ is Rust's closes competitor in this domain space and it suffers from terribly slow compiling as well.
Rust's compile times are notoriously slow. Rust development was slow enough on my laptop that I finally gave up on mobile computing and bought a desktop with a top-of-the line CPU (12900K). Along the way I switched from Windows to Linux (more on that later) and started using the mold linker, and now… things are OK!
Moreover, it is slow. Rust is a snail compared to other languages. Even for small projects, the compile times are painfully long, and runtime measurements show that Rust is less efficient than the C programs.
You can easily notice similarities between Rust and C++ syntax, but Rust offers a higher level of memory safety without using a garbage collector. Not for the first time, Rust has been named the most loved language—it gained more than 86% of developers' votes.
The difference is of course -march=native
... kind of. Rust has this through -C target_cpu=native
, but this doesn't give the same speed benefit. This is because LLVM is unwilling to vectorize in this context, whereas GCC does. You may note that using Clang, a C++ compiler that also uses LLVM, also produces relatively slow code.
To encourage LLVM to vectorize, you can move the main loop into a separate function. Alternatively, you can use a local block. If you write the code carefully as
let dist = {
let mut dist = i32::max_value();
for &(a, b) in &points_a[..n] {
for &(c, d) in &points_b[..n] {
dist = std::cmp::min(((a - c).abs() + (b - d).abs()), dist);
}
}
dist
} as u32;
the difference between Rust and C++ is then near-negligible (~4%).
The vast majority of the performance you're seeing in C++ is due to the flag -march=native
.
This flag is not the equivalent flag to Rust's --release
. It uses CPU instructions specific to the CPU it is compiled on, so math in particular is going to be way faster.
Removing that flag puts the C++ code at 19 seconds.
Then there's the unsafety present in the C++ code. None of the input is checked. The Rust code does check it, you use .unwrap()
– unwrap
has a performance cost, there's an assertion, then the code necessary for unwinding, etc.
Using if let
s instead of raw unwrap
s, or ignoring results where possible, brings the Rust code down again.
Rust: 22 seconds
C++: 19 seconds
Where's the 3 seconds coming from? A bit of playing around leads me to believe it's println!
vs. printf
, but I don't have hard numbers for the C++ code. What I can say is that the Rust code drops to 13 seconds when I perform the printing outside of the benchmark.
TLDR: Your compiler flags are different, and your C++ code is not safe.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With