RustBrock/The Performance Closures and Iterators.md
darkicewolf50 f3ec0d98cc
Some checks failed
Test Gitea Actions / first (push) Successful in 12s
Test Gitea Actions / check-code (push) Failing after 21s
Test Gitea Actions / test (push) Has been skipped
Test Gitea Actions / documentation-check (push) Has been skipped
finsihed ch14.1 and more than halfway through ch14.2
2025-02-25 17:02:50 -07:00

4.0 KiB
Raw Permalink Blame History

Comparing Performance: Loops vs. Iterators

To determince whether to use loops or iterators, you need to know which implementation is faster

Is it the version with an explicit for loop?

Is it the version with iterators?

Here is a benchmark where we load the entire contnets of The Adventures of Sherlock Holmes by Sir Arthur Conan Doyle into a String and looking ofr the word the in the contents

Here is the results of the benchmark on the version of search using the for loop and the version using iterators:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

The two have similar performace!

We won't explain the benchmark code here, because the poin is not to prive that the two versions are equivalent but to get a general sense of how these two compare performance-wise.

For a more comprehensive benchmark, you should check using various texts of various sizes as the contents, different words and words of different lenghts as the query and all kinds of other variations.

The point is this, iterators, although a high-level abstraction, get compiled down to roughly the same code as if you wrote the lower-level code yourself.

Iterators are one of Rust's *zero-cost abstractions`. This means using the abstraction imposes no addtional runtime overhead.

This is analogous to how Bjarne Stroustrup, the original designer and implementor of C++ defines zero-verhead in "Fondations of C++" (2012)

In general, C++ implementations obey the zero-overhead principle: What you dont use, you dont pay for. And further: What you do use, you couldnt hand code any better.

Here is another example, the code is taken from an audio decoder.

The decoding algorithm uses the linear prediction mathematical operation to estimate future values based on a linear function of the previous samples.

This code uses an iterator chain to do some math on three variables in scope:

  • a buffer slice of data
  • an array of coefficients
  • an amount ot which shift data in qlp_shift

We declared the variables within this example but not given them any values.

Even though this code doesnt have muc meaning outside of its context, it is still a concise, real-world example of how Rust tranlate high-level ideas to low-level code.

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

To calcuate the value of prediction, the code iterates though each of he 12 values in coefficients and ses the zip method to pair the coefficient values with the previous 12 values in buffer

For each pair then we multiply the values together, sum all the results and shift the bits in the sum qlp_shift bits to the right.

Calculations in applications like audio decoders often prioritize perfomance.

Here we are creating an iterator, using two adapters and then consuming the value.

What assembly would this Rust code compile into.

As of writing this in the book, it compiles down to the same assbly you would write by hand.

There is no loop at all corresponding to the iteration over the values in coefficients.

Rust knows that there are 12 iterations, so it "unrolls" the loop.

Unrolling is an optimization that removes the overhead of the loop controlling code and instead generates repetitive code for each iteration fo the loop.

All of the coefficients ge stored in registers which means accessing the values is very fast.

There are no bounds checks on the array access at runtime.

All of these optimizations that Rust is able to apply make the resulting code extremely efficient.

Now you know that you can use iterators and closures without fear of performance hits.

They make code seem like its highe level but again dont impose a performance hit.