Should I use map or a for loop?

In general, is there a preferred way to generate a vector when it comes to using either map or a for loop? For example, if i wanted to create a vector of vectors i could do

(0..n).map(|idx| (0..n).map(|jdx| some_function(idx,jdx)).collect::<Vec<f32>>()).collect::<Vec<Vec<f32>>>();

or i could

let mut result = vec![vec![0.0;n];n];
for idx in 0..n {
for jdx in 0..n {
result[idx][jdx] = some_function(idx,jdx);
}
}

Is there difference in performance or one which is used more commonly?

Thanks

The iterator solution might have fewer bounds checks on the vector, but I think either is fine.

You can eliminate the extra bounds checks in the loop like this:

let mut result = vec![vec![0.0;n];n];
for (idx, row) in result.iter_mut().enumerate() {
    for (jdx, cell) in row.iter_mut().enumerate() {
        *cell = some_function(idx,jdx);
    }
}
14 Likes

I think many here would say the 'map' solution is more idiomatic Rust.

Personally I vote for Alice's 'for' loop solution. It says what it does more clearly, and does not have the conceptual overheads of 'map, lambda's, 'collect to think about. Or all those noisy type annotations in the way.

2 Likes

alice probably has the best answer (readability/performance), but I tend to prefer functional solutions (e.g. doesn't mutate the *cell) if at all possible. (I have no good reason to - I just do).

BUT, one thing I'd say is the Vec<Vec<T>> is less efficient than Vec<T> where the width,height (row/col, idx,jdx) are computed externally. I do a lot of image processing, and we rarely perform a double-for loop.. we generally do a single stride offset from a cursor (since, if I'm brightening all pixels, I don't CARE what the width/height is; I just want to hit every element in the array).. If I force a 2D structure like this (AND require a double indirection) I'm introducing an unnecessary inefficiency at each row boundary.

From that personal bias, I tend to make all my 2,3,4 D arrays a 1D array; and the above problem becomes moot. You can always have a fn get(&self,x:usize,y:usize) -> T { self.data[y*self.WIDTH+x] } call to provide an easy to use API.. Similarly you an easily produce iterators over a single row. BUT you could also make an iterator over a single COLUMN as well (something not efficient to perform with a Vec<Vec<T>>).

I think my bias came from doing image processing in Java, where 2D arrays are basically the same as Vec<Vec<T>>.. unlike C/C++ where you can make them a single computed value. Happens to suite my rust code just fine. :slight_smile:

6 Likes

If n is const, you can use std::array::from_fn:

let res: [[f32; N]; N] = std::array::from_fn(|idx|
    std::array::from_fn(|jdx| some_function(idx,jdx))
);
4 Likes

That's super nice! Did not know this function existed in std!

One interesting thing I found is how from_fn() is implemented

pub fn from_fn<T, const N: usize, F>(mut cb: F) -> [T; N]
where
    F: FnMut(usize) -> T,
{
    let mut idx = 0;
    [(); N].map(|_| {
        let res = cb(idx);
        idx += 1;
        res
    })
}

It turns out you can map an array of one type to an array of another type without unsafe code! Neat!

2 Likes

Yes, map on arrays was stabilized in 1.55. Pretty awesome! array - Rust

Well, there's a bunch of unsafe underneath it, of course.

If you're curious about implementation details, I've got a PR open drastically changing it to work better: https://github.com/rust-lang/rust/pull/107634

3 Likes

Oh that's really cool!

How drastic are the speed ups? Is there a huge difference in runtime? The asm definitely looks a lot cleaner!

As always with μoptimizations like this, it will depend greatly on the situation. Something with a big body, like .map(fs::read_to_string) probably will show no difference at all.

But for something with a trivial body, it can matter. I tossed together this (silly) benchmark:

#[bench]
fn bench_array_map_add_pi(b: &mut Bencher) {
    let mut a: [f32; 64] = black_box(core::array::from_fn(|i| i as _));
    b.iter(|| {
        a = black_box(a.map(|x| x + 3.14));
    });
}

And it said 3.3× faster:

BEFORE:
test bench_array_map_add_one               ... bench:      57 ns/iter (+/- 0)

AFTER:
test bench_array_map_add_one               ... bench:      17 ns/iter (+/- 0)
2 Likes

Of course, but that is still an amazing speed up! I am awful at statistics, but this does seem like it's significant!

I dont know if your example is just coincidence but if your actual use case is a dynamically sized two dimensional array you will get the biggest performance benefit by using a single allocation instead of nested vectors. You might want to have a look at ndarray.

To add to this, a Vec<Vec<T>> can have differently sized "rows" whereas with a 1D structure you know everything is as it should be if the length matches width * height

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.