I have a boring common borrowing problem, and I'd like to know if you have suggestions and improvements.
I am trying to translate to Rust this little C++ program:
https://sourceforge.net/p/ctidbits/code/HEAD/tree/trunk/discrepancy/discrepancy.cpp
The input file is:
https://sourceforge.net/p/ctidbits/code/HEAD/tree/trunk/discrepancy/discrepancy.dat?force=True
The C++ code (with tiny changes):
#include <fstream>
#include <vector>
#include <algorithm>
#include <cmath>
struct Point {
double x, y;
bool marked;
Point(double x_, double y_) : x(x_), y(y_), marked(false) {}
Point(const Point &p) : x(p.x), y(p.y), marked(p.marked) {}
};
std::vector<Point> points;
std::vector<Point*> sortX, sortY, subSetY;
double maxDiscrepancy, recMinX, recMinY, recMaxX, recMaxY;
void readPoints() {
std::ifstream inp("discrepancy.dat");
double x, y;
while (inp >> x >> y)
points.push_back(Point(x, y));
inp.close();
printf("Read %u points from input file\n", points.size());
}
bool pointCompX(const Point *a, const Point *b) { return a->x < b->x; }
bool pointCompY(const Point *a, const Point *b) { return a->y < b->y; }
void sortPoints() {
sortX.resize(points.size());
sortY.resize(points.size());
for (size_t i = 0; i < points.size(); i++) {
sortX[i] = &points[i];
sortY[i] = &points[i];
}
std::sort(sortX.begin(), sortX.end(), pointCompX);
std::sort(sortY.begin(), sortY.end(), pointCompY);
}
void innerLoopY(int n, double lowX, double highX) {
// printf("inner loop %d\n", n);
const double xDim = highX - lowX;
for (int i = 0; i < n; ++i) {
double lowY = subSetY[i]->y;
for (int j = i; j < n; ++j) {
const double highY = subSetY[j]->y;
const double yDim = highY - lowY;
const int count = j + 1 - i;
const double Discrepancy = fabs(points.size() * xDim * yDim - count);
if (Discrepancy > maxDiscrepancy) {
maxDiscrepancy = Discrepancy;
recMinX = lowX;
recMinY = lowY;
recMaxX = highX;
recMaxY = highY;
}
}
}
}
void outerLoopX() {
subSetY.resize(points.size());
maxDiscrepancy = 0.0;
for (size_t i = 0; i < points.size(); ++i) {
printf("Low X = %u\n", i);
double lowX = sortX[i]->x;
for (size_t j = i; j < points.size(); ++j) {
// printf("High X = %u\n", j);
sortX[j]->marked = true;
double highX = sortX[j]->x;
int l = 0;
for (size_t k = 0; k<points.size(); ++k)
if (sortY[k]->marked) {
subSetY[l] = sortY[k];
l++;
}
innerLoopY(l, lowX, highX);
}
for (size_t j = i; j < points.size(); ++j)
sortX[j]->marked = false;
}
}
int main() {
readPoints();
sortPoints();
outerLoopX();
printf("Maximum Discrepancy = %f\n", maxDiscrepancy);
printf("Rect %f %f %f %f\n", recMinX, recMinY, recMaxX, recMaxY);
}
As you see it contains four arrays (vectors): pointlist that contains the input data points. Its x and y coordinates never change once loaded, only Point.marked changes. There are also sortX and sortY arrays that contain pointers to pointlist (and this causes borrowing problems in Rust) that point to Points that are sorted according to the x or y field. And there's subSetY, another array of pointers that are sorted by x field, and are a shrinking selection (subset) of the Points.
Minutes after starting a Rust translation of the C++ code I've seen the borrowing problems, so I've soon modified the algorithm, replacing the pointers with array indexes, and I have also removed the global variables to make the Rust code nicer (equally removing the global variables in the C++ code makes it a little faster):
struct Point {
x: f64,
y: f64,
marked: bool
}
fn read_data() -> Vec<Point> {
use std::fs::File;
use std::io::Read;
let mut data = String::new();
File::open("discrepancy.dat").unwrap().read_to_string(&mut data).unwrap();
let points: Vec<Point> =
data
.lines()
.map(|line| line.split(' ').map(|p| p.parse().unwrap()))
.map(|mut parts| Point { x: parts.next().unwrap(),
y: parts.next().unwrap(),
marked: false })
.collect();
println!("Read {} points from input file", points.len());
points
}
fn sort_points(points: &[Point]) -> (Vec<usize>, Vec<usize>) {
let mut sort_x: Vec<usize> = (0 .. points.len()).collect();
sort_x.sort_by(|&i, &j| points[i].x.partial_cmp(&points[j].x).unwrap());
let mut sort_y: Vec<usize> = (0 .. points.len()).collect();
sort_y.sort_by(|&i, &j| points[i].y.partial_cmp(&points[j].y).unwrap());
(sort_x, sort_y)
}
struct DiscrepancyRes {
max_discrepancy: f64,
rec_min_x: f64,
rec_min_y: f64,
rec_max_x: f64,
rec_max_y: f64,
}
fn inner_loop_y(n: i32, low_x: f64, high_x: f64, points: &[Point],
sub_set_y: &[usize], res: &mut DiscrepancyRes) {
let x_dim = high_x - low_x;
for i in 0 .. n {
let low_y = points[sub_set_y[i as usize]].y;
for j in i .. n {
let high_y = points[sub_set_y[j as usize]].y;
let y_dim = high_y - low_y;
let count = j + 1 - i;
let discrepancy = (points.len() as f64 * x_dim * y_dim - count as f64).abs();
if discrepancy > res.max_discrepancy {
*res = DiscrepancyRes {
max_discrepancy: discrepancy,
rec_min_x: low_x,
rec_min_y: low_y,
rec_max_x: high_x,
rec_max_y: high_y,
};
}
}
}
}
fn outer_loop_x(points: &mut [Point], sort_x: &[usize], sort_y: &[usize]) -> DiscrepancyRes {
let mut res = DiscrepancyRes {
max_discrepancy: 0.0,
rec_min_x: 0.0, rec_min_y: 0.0,
rec_max_x: 0.0, rec_max_y: 0.0,
};
let mut sub_set_y = vec![0; points.len()];
for i in 0 .. points.len() {
println!("Low X = {}", i);
let low_x = points[sort_x[i]].x;
for j in i .. points.len() {
points[sort_x[j]].marked = true;
let high_x = points[sort_x[j]].x;
let mut n = 0;
for k in 0 .. points.len() {
if points[sort_y[k]].marked {
sub_set_y[n as usize] = sort_y[k];
n += 1;
}
}
inner_loop_y(n, low_x, high_x, points, &sub_set_y, &mut res);
}
for &px in sort_x {
points[px].marked = false;
}
}
res
}
fn main() {
let mut points = read_data();
let (sort_x, sort_y) = sort_points(&points);
let res = outer_loop_x(&mut points, &sort_x, &sort_y);
println!("Maximum Discrepancy = {:.6}", res.max_discrepancy);
println!("Rect {:.6} {:.6} {:.6} {:.6}",
res.rec_min_x, res.rec_min_y, res.rec_max_x, res.rec_max_y);
}
Unfortunately this Rust code is a bit too much slower than the C++ version (with the standard input file the C++ code runs in 1.42 seconds, this first Rust version runs in about 2.05 seconds), probably because of the double indirection caused by the points array plus array bounds tests.
So I've rewritten the Rust code using raw pointers and lot of unsafety:
#![allow(trivial_casts)]
struct Point {
x: f64,
y: f64,
marked: bool
}
fn read_data() -> Vec<Point> {
use std::fs::File;
use std::io::Read;
let mut data = String::new();
File::open("discrepancy.dat").unwrap().read_to_string(&mut data).unwrap();
let points: Vec<Point> =
data
.lines()
.map(|line| line.split(' ').map(|p| p.parse().unwrap()))
.map(|mut parts| Point { x: parts.next().unwrap(),
y: parts.next().unwrap(),
marked: false })
.collect();
println!("Read {} points from input file", points.len());
points
}
unsafe fn sort_points(points: &mut [Point]) -> (Vec<*mut Point>, Vec<*mut Point>) {
let mut sort_x: Vec<*mut Point> = points.iter_mut().map(|r| r as *mut Point).collect();
sort_x.sort_by(|&a, &b| (*a).x.partial_cmp(&(*b).x).unwrap());
let mut sort_y: Vec<*mut Point> = points.iter_mut().map(|r| r as *mut Point).collect();
sort_y.sort_by(|&a, &b| (*a).y.partial_cmp(&(*b).y).unwrap());
(sort_x, sort_y)
}
struct DiscrepancyRes {
max_discrepancy: f64,
rec_min_x: f64,
rec_min_y: f64,
rec_max_x: f64,
rec_max_y: f64,
}
unsafe fn inner_loop_y(n: i32, low_x: f64, high_x: f64, points: &[Point],
sub_set_y: &[*mut Point], res: &mut DiscrepancyRes) {
let x_dim = high_x - low_x;
for i in 0 .. n {
let low_y = (*sub_set_y[i as usize]).y;
for j in i .. n {
let high_y = (*sub_set_y[j as usize]).y;
let y_dim = high_y - low_y;
let count = j + 1 - i;
let discrepancy = (points.len() as f64 * x_dim * y_dim - count as f64).abs();
if discrepancy > res.max_discrepancy {
*res = DiscrepancyRes {
max_discrepancy: discrepancy,
rec_min_x: low_x,
rec_min_y: low_y,
rec_max_x: high_x,
rec_max_y: high_y,
};
}
}
}
}
unsafe fn outer_loop_x(points: &[Point],
sort_x: &[*mut Point], sort_y: &[*mut Point]) -> DiscrepancyRes {
let mut res = DiscrepancyRes {
max_discrepancy: 0.0,
rec_min_x: 0.0, rec_min_y: 0.0,
rec_max_x: 0.0, rec_max_y: 0.0,
};
let mut sub_set_y = vec![std::ptr::null::<Point>() as *mut Point; points.len()];
for i in 0 .. points.len() {
println!("Low X = {}", i);
let low_x = (*sort_x[i]).x;
for j in i .. points.len() {
(*sort_x[j]).marked = true;
let high_x = (*sort_x[j]).x;
let mut n = 0;
for k in 0 .. points.len() {
if (*sort_y[k]).marked {
sub_set_y[n as usize] = sort_y[k];
n += 1;
}
}
inner_loop_y(n, low_x, high_x, points, &sub_set_y, &mut res);
}
for &px in sort_x {
(*px).marked = false;
}
}
res
}
fn main() {
let mut points = read_data();
let res = unsafe {
let (sort_x, sort_y) = sort_points(&mut points);
outer_loop_x(&points, &sort_x, &sort_y)
};
println!("Maximum Discrepancy = {:.6}", res.max_discrepancy);
println!("Rect {:.6} {:.6} {:.6} {:.6}",
res.rec_min_x, res.rec_min_y, res.rec_max_x, res.rec_max_y);
}
Regarding this second Rust version, do you know if there are better ways to write these two lines of code (the #![allow(trivial_casts)] is needed by the first line)?
let mut sort_x: Vec<*mut Point> = points.iter_mut().map(|r| r as *mut Point).collect();
let mut sub_set_y = vec![std::ptr::null::<Point>() as *mut Point; points.len()];
This second Rust versions runs in about 1.85 seconds, it's slower than the C++ code, but the difference is more acceptable. How to improve this second Rust version and remove all or most unsafety? Most of the slowdown is in the inner_loop_y() function. I've asked on #IRC, and I've modified the code according to an answer by vfs:
#[derive(Clone, Copy)]
struct Point {
x: f64,
y: f64,
marked: bool
}
fn read_data() -> Vec<Point> {
use std::fs::File;
use std::io::Read;
let mut data = String::new();
File::open("discrepancy.dat").unwrap().read_to_string(&mut data).unwrap();
let points: Vec<Point> =
data
.lines()
.map(|line| line.split(' ').map(|p| p.parse().unwrap()))
.map(|mut parts| Point { x: parts.next().unwrap(),
y: parts.next().unwrap(),
marked: false })
.collect();
println!("Read {} points from input file", points.len());
points
}
fn sort_points(mut points: Vec<Point>) -> (Vec<Point>, Vec<usize>) {
points.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
let mut sort_y: Vec<usize> = (0 .. points.len()).collect();
sort_y.sort_by(|&i, &j| points[i].y.partial_cmp(&points[j].y).unwrap());
(points, sort_y)
}
struct DiscrepancyRes {
max_discrepancy: f64,
rec_min_x: f64,
rec_min_y: f64,
rec_max_x: f64,
rec_max_y: f64,
}
unsafe fn inner_loop_y(n: i32, low_x: f64, high_x: f64, points: &[Point],
sub_set_y: &[*const Point], res: &mut DiscrepancyRes) {
let x_dim = high_x - low_x;
for i in 0 .. n {
let low_y = (*sub_set_y[i as usize]).y;
for j in i .. n {
let high_y = (*sub_set_y[j as usize]).y;
let y_dim = high_y - low_y;
let count = j + 1 - i;
let discrepancy = (points.len() as f64 * x_dim * y_dim - count as f64).abs();
if discrepancy > res.max_discrepancy {
*res = DiscrepancyRes {
max_discrepancy: discrepancy,
rec_min_x: low_x,
rec_min_y: low_y,
rec_max_x: high_x,
rec_max_y: high_y,
};
}
}
}
}
unsafe fn outer_loop_x(sort_x: &mut [Point], sort_y: &[usize]) -> DiscrepancyRes {
let mut res = DiscrepancyRes {
max_discrepancy: 0.0,
rec_min_x: 0.0, rec_min_y: 0.0,
rec_max_x: 0.0, rec_max_y: 0.0,
};
let mut sub_set_y: Vec<*const Point> = Vec::with_capacity(sort_x.len());
for i in 0 .. sort_x.len() {
println!("Low X = {}", i);
let low_x = sort_x[i].x;
for j in i .. sort_x.len() {
sort_x[j].marked = true;
let high_x = sort_x[j].x;
let mut n = 0;
sub_set_y.clear();
for k in 0 .. sort_x.len() {
if sort_x[sort_y[k]].marked {
sub_set_y.push(&sort_x[sort_y[k]]);
n += 1;
}
}
inner_loop_y(n, low_x, high_x, sort_x, &sub_set_y, &mut res);
}
for j in 0 .. sort_x.len() {
sort_x[j].marked = false;
}
}
res
}
fn main() {
unsafe {
let (mut sort_x, sort_y) = sort_points(read_data());
let res = outer_loop_x(&mut sort_x, &sort_y);
println!("Maximum Discrepancy = {:.6}", res.max_discrepancy);
println!("Rect {:.6} {:.6} {:.6} {:.6}",
res.rec_min_x, res.rec_min_y, res.rec_max_x, res.rec_max_y);
}
}
This third Rust version contains less unsafety. The data is owned by the sorted array of Points sort_x, and I've removed the points array. The sort_y is an array of indexes, and to keep performance inner_loop_y() receives a sub_set_y that contains pointers (but now they can be const). The performance is about the same as the second Rust version, it runs in about 1.86 seconds.
Do you have suggestions for improvements and to keep (or improve) speed and remove some (or all) unsafety?