Reputation: 2574
I'm experimenting with rewriting some old C code into Rust - which I'm new to. One recurring issue I have is that the C code has a lot of loops like this:
for (i = startIndex; i < asize; i++)
{
if (firstEdge < 0 && condLeft(i))
{
firstEdge = i;
}
RightIndex = asize-1-i;
if (firstEdgeRight < 0 && condRight(RightIndex))
{
firstEdgeRight = RightIndex;
}
// Found both edges
if (firstEdge >= 0 && firstEdgeRight >= 0) {
break;
}
}
How would you translate that into Rust in a performant way? My issue is that while I could probably get the functionality I want I'm not sure how to do it obtain (roughly) the same speed.
This part of the code is the main bottleneck in our code (this part of the code at least) and when translating it would like to keep the following properties.
asize
can be very large.firstEdge
and firstEdgeRight
are found roughly at the same time. Therefore it has been a good thing to only have one loop instead of two - in order to avoid search from the beginning again (even though I think this solution kills the prefetcher (but I'm not sure, maybe the old machine running the code doesn't even have a prefetcher)).While performance is important, readability is of course even more important :)
EDIT Ok, here is a possible Rust implementation by me (cond_right()
and cond_left()
are left out).
The things I think about is:
first_edge
and first_edge_right
mutable? They are in my implementation, but it feels wrong to me since they are only assigned once.let mut first_edge = -1;
let mut first_edge_right = -1;
// Find first edge
let start_index = 300; // or something
let asize = 10000000;
for i in start_index..asize {
if first_edge < 0 && cond_left(i) {
first_edge = i;
}
let right_index = asize - i -1;
if first_edge_right < 0 && cond_right(right_index) {
first_edge_right = right_index;
}
if (first_edge >= 0 && first_edge_right >= 0) {
break;
}
}
Upvotes: 1
Views: 459
Reputation: 432139
You need to be prepared to make a choice:
How would you translate that into Rust in a performant way?
while performance is important, readability is of course even more important
Which is actually more important to you? Here's how I would write the code, assuming that I've understood what you are asking for:
fn left_condition(i: usize) -> bool {
i > 1000
}
fn right_condition(i: usize) -> bool {
i % 73 == 0
}
fn main() {
let start_index = 300;
let asize = 10000000;
let left = (start_index..asize).position(left_condition);
let right = (start_index..asize).rev().position(right_condition);
println!("{:?}, {:?}", left, right);
}
We iterate once from left-to-right and once from right-to-left. My gut tells me that this will provide code with simple branch prediction that accesses memory in a linear manner, both of which should be optimizable.
However, the variable name asize
gives me pause. It certainly sounds like an abbreviation of "array size". If that's the case, then I would 100% recommend using slices instead of array indices. Why? Because array access (foo[0]
) usually has overhead of bounds checking. I'd write something with slices:
let data = vec![0; 10_000_000];
let start_index = 300;
let slice = &data[start_index..];
let left = slice.iter().position(|&i| left_condition(i));
let right = slice.iter().rev().position(|&i| right_condition(i));
However, there's only one possible true answer to your question:
Use a profiler
Only knowing your actual data, your actual implementations of the conditions, the rest of the code you are running, etc., can you actually know how fast something will be.
Therefore it has been a good thing to only have one loop instead of two - in order to avoid search from the beginning again
This is nonintuitive to me, so I'd want to see profiling results that back up the claim.
Upvotes: 5
Reputation: 12567
cond_left
and cond_right
are important to answer the performance question. For example, will replacing an index with an iterator help? Not going to tell without knowing what cond_left
does.
The other concern you have is that first_edge
and first_edge_right
are mutable. The proposed RFC allowing loops to return a value could be an elegant way to solve this problem. Right now you could emulate the loop return with a closure:
let (_first_edge, _first_edge_right): (i32, i32) = (||{
let (mut first_edge, mut first_edge_right) = (None, None);
// ...
return (first_edge.unwrap(), first_edge_right.unwrap());
})();
Replacing -1
with None
will likely make the variable larger. See Can I use the "null pointer optimization" for my own non-pointer types?.
Splitting this loop into two loops, one getting the first_edge
and another examining the remaining range to get the first_edge_right
seems like the right thing to do, but the CPU branch prediction will likely minimize the impact.
Upvotes: 2
Reputation: 13626
Since it is critical to keep looking from both sides simultaneously, I don’t think there is an easy way to avoid having mutable variables.
One thing that can improve readability is to use option
instead of negative numbers. Otherwise the code is fine.
(Another thing you probably could do is to break the loop when the indices meet in the middle, if this means that there is no solution for your problem, but that’s not Rust specific.)
Upvotes: 0