DEV Community

Cover image for Optimising Code - The Devil Lies In the Details
Maximilian for LibRapid

Posted on • Updated on

Optimising Code - The Devil Lies In the Details

Optimising code for high performance can be exhausting; where can you squeeze that last bit of speed out of your code? How can you be faster than that one piece of code you found online?

For that, let's take a look at the following example of our Rust crate:

pub fn new(values: &Vec<f64>) -> MathVector {
    let mut len: f64 = 0;
    for i in values {
        len += i * i;
    }
    MathVector { values:    values.clone(),
                 dimension: values.len(),
                 length:    len.sqrt() }
}
Enter fullscreen mode Exit fullscreen mode

Among other things - like the for loop for example - one thing is the problem. Can you spot it? The answer is: len.sqrt(). Getting the square root of a number is performance-wise extremely expensive - how should we get rid of it? You might guess, that we could implement something like the infamous "Fast inverse square root" algorithm from Quake III. This has one problem though: It's inaccurate. And for something like mathematical applications, this is sometimes very unwanted. So we need to solve this in another way.
I got it: Why don't we calculate it only when the value is actually needed? Let's do it!
But firstly, we need to find a value for this field which cannot be occupied by pure chance. 0 is no candidate, as a vector may have the length 0. But what about a negative length? That sounds good. You are not able to have a negative length of a vector because of the nature of mathematics; the length of a vector is calculated by the sum of the squares of all values, and then taking the square root. As the value of a square root is - by definition - never negative, we can say with confidence that a negative number is ideal. Let's take -1. In case of Rust though, we don't need to worry about that; we can just use a option<T> to represent the absence of a value.
Now we need to generate a new getter for the field length, which only calculates it when actually needed. The code looks like this:

pub fn length(self: &mut Self) -> f64 {
    match self.length {
        None      => { let mut len: f64 = 0f64; 
                       for i in &self.values {
                           len += i * i;
                       }
                       len = len.sqrt();
                       self.length = Some(len);
                       return len;
                     }
        Some(len) => { return len; }

        }
}
Enter fullscreen mode Exit fullscreen mode

As you can clearly see, we check if the length is already set using the logic from our thought; if so, we can just return that. If it is None (or -1 in other implementations), we just calculate the length, store it and return the newly calculated length.
And that's it! With just a few more lines of code, we removed unneccessary computations by calculating something only when it is needed. Feel inspired to apply that whereever you can!

Discussion (1)

Collapse
pencilcaseman profile image
Pencilcaseman

Nice article!