A Faster Loop-The-Loop

Let’s make things better today.

We’re humans. We’re horrible at repetition. We like entertainment. For repetitive tasks, we have tools like loops that we can utilise.

An example from BrewStillery looked like this originally:

pub fn increaseABVMaths(allInputs: &increaseABVData) -> finalSugarFloat {
    let mut newStartingBrix: f64 = allInputs.startingBrix;

    let mut newEstimatedABV: f64 = realABVAndAttenuation(newStartingBrix, FINAL_BRIX_IDEAL).0;

    while newEstimatedABV < allInputs.desiredABV {
        newStartingBrix = newStartingBrix + 0.001;
        newEstimatedABV = realABVAndAttenuation(newStartingBrix, FINAL_BRIX_IDEAL).0;
    }
    ...

What’s going on is newStartingBrix is a user inputted f64.

newEstimatedABV is set to be the ABV return value of the function realABVAndAttenuation().

Then, we say while newEstimatedABV is less than the user’s desired ABV, increment newStartingBrix by 0.001, use that value as an input in realABVAndAttenuation(), and store the resultant ABV in newEstimatedABV.

To our perception, this works basically instantly on modern hardware. We can still improve things though. Rust has a fantastic crate called Rayon, which allows us to parallelise Rust’s .iter() method. We do that by changing it from .iter() to .par_iter(). It’s as straightforward as it can be.

The issue is that nowhere in the Rust documentation does it say anything about parallelising a while loop. There are great examples of for loops, but that doesn’t help us here.

What to do?

Well, we have to work with some goofy bounds. In Rust, iterators are only allowed to use integers. So we can’t create one like 0.0..33.0.

In this particular example, I know the range that brix can be. It is a sugar density which is measured by a refractometer. It’s maximum value is 32.

If we employ a bit of mathy inversion trickery, we can come up with something like this: (newStartingBrix * 1000.0) as u32)..33000).

All we’re doing here is getting rid of the decimal (f64) and turning it into a large integer (u32).

Then we convert it back to a float inside the .map() by doing this: let tempBrix: f64 = ( mapBrix as f64 / 1000.0 ) + 0.0001;

I changed the range from 32 to 33 because I wanted a little more buffer on the top end, and I love 3’s.

Something to note, is that if our iterator goes past 33000, this will crash our program. There are input guards in a preceeding function that rejects any input greater than 32.

The final code looks like this:

pub fn increaseABVMaths(allInputs: &increaseABVData) -> finalSugarFloat {
    let mut newStartingBrix: f64 = allInputs.startingBrix;

    newStartingBrix = (((newStartingBrix * 1000.0) as u32)..33000)
        .into_par_iter()
        .map(|mapBrix| {
            let tempBrix: f64 = ( mapBrix as f64 / 1000.0 ) + 0.0001;
            let tempABV: f64 = realABVAndAttenuation(tempBrix,FINAL_BRIX_IDEAL).0;

            (tempBrix, tempABV)
        })
        .find_first(|(_tempBrix, tempABV)| {
            allInputs.desiredABV < *tempABV
        })
        .expect("increaseABVPrep(), newEstimatedABV")
        .0;
    ...

To run through two for loop examples, we can parallelise our twoArraySum():

pub fn twoArraySum(firstArray: [f64; 81], secondArray: [f64; 81]) -> f64 {
    let mut sum: f64 = 0.0;

    // this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
    for index in 0..81 {
        sum = sum + firstArray[index] * secondArray[index];
    }

    sum
}

Which becomes:

pub fn twoArraySum(firstArray: [f64; 81], secondArray: [f64; 81]) -> f64 {
// this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
    firstArray
        .par_iter()
        .zip(secondArray.par_iter())
        .map(|(first, second)|{
            first * second
        }).sum()
}

If you’re not familiar with it, the .zip() method pairs up iterators. We then throw that into .map() and just replicate the maths from the original for loop.

And finally, our threeArraySum():

pub fn threeArraySum(firstArray: [f64; 81], secondArray: [f64; 81], thirdArray: [f64; 81]) -> f64 {
    let mut sum: f64 = 0.0;

    // this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
    for index in 0..81 {
        sum = sum + firstArray[index] * secondArray[index] * thirdArray[index];
    }

    sum

Becomes:

pub fn threeArraySum(firstArray: [f64; 81], secondArray: [f64; 81], thirdArray: [f64; 81]) -> f64 {
    // this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
    firstArray
        .par_iter()
        .zip(secondArray.par_iter())
        .zip(thirdArray.par_iter())
        .map(|((first, second), third)|{
            first * second * third
        }).sum()

The unique thing here is what’s going in .map(). The syntax for mapping multiple .zip()’s is a bit weird. It only accepts a tuple, so we have to group them in nested pairs. If we were going to do a fourth iterator, it would look like this: (((first, second), third), fourth) for the .map() arguments.

Now, we’ve safely

AFasterLoopPlaid

Meade Written by:

Technology Omnivore

comments powered by Disqus