Difference between .map() and MaybeUinit when iterating over slices

I have two slices and need to perform element wise multiplication. The simplest and probably the best way is to iterate over the slices and map, collect the result into a vector. For ex,

pub fn mul_map(a: &[u64], b: &[u64]) -> Vec<u64> {
    a.iter()
        .zip(b.iter())
        .map(|(va, vb)| va * vb)
        .collect::<Vec<u64>>()
}

Another way, and more complicated, is to use MaybeUinit like following

pub fn mul_maybeuinit(res: &mut [std::mem::MaybeUninit<u64>], a: &[u64], b: &[u64]) {
    // we can assume that the caller knows the exact length of result and has initialised a new vec with Vec::with_capacity
    res.iter_mut()
        .zip(a.iter().zip(b.iter()))
        .for_each(|(r, (va, vb))| {
            r.write(va * vb);
        })
}

Both versions seem to have same performance. Which makes me curious about how does one differ from another in terms of performing the oprations in closure and allocating data in memory?

It seems to me that the main difference is that mul_map will allocate memory for a vector, which the other option wont.

ok thanks! So if I call let res = Vec::with_capacity inside mul_maybeuinit then both functions are same ?

I would like to propose an alternative, which doesn't use MaybeUninit but might give you some flexibility:

pub fn mul_map<'a>(a: &'a [u64], b: &'a [u64]) -> impl 'a + Iterator<Item = u64> {
    a.iter()
        .zip(b.iter())
        .map(|(va, vb)| va * vb)
}

pub fn mul_map_alloc(a: &[u64], b: &[u64]) -> Vec<u64> {
    mul_map(a, b).collect()
}

pub fn mul_map_reuse(vec: &mut Vec<u64>, a: &[u64], b: &[u64]) {
    vec.clear();
    vec.extend(mul_map(a, b));
}

(Playground)


Or maybe even more generic:

pub fn mul_map<'a, A, B>(a: A, b: B) -> impl 'a + Iterator<Item = u64>
where
    A: 'a + IntoIterator<Item = &'a u64>,
    B: 'a + IntoIterator<Item = &'a u64>,
{
    a.into_iter()
        .zip(b.into_iter())
        .map(|(va, vb)| va * vb)
}

(Playground)

3 Likes

What do you mean exactly?

I meant the following

I think that example misses to set the length (which requires unsafe).

1 Like

oops, I missed it. Thanks!

I suspect both functions will more or less behave the same then, though I have sometimes been surprised by optimizations that Rust does or doesn't do. If in doubt, you can also try to check the assembler output.

1 Like

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.