I just had a random thought: a common pattern in Rust is to things such as:

let vec_a: Vec<String> = /* ... */;
let vec_b: Vec<String> = vec_a.into_iter().filter(some_filter).collect();

Usually, we need to be aware of the fact that Iterator::collect() allocates for the container we are collecting into. But in the snippet above, we’ve consumed a container of the same type. And since Rust has full ownership of the vector, in theory the memory allocated by vec_a could be reused to store the collected results of vec_b, meaning everything could be done in-place and no additional allocation is necessary.

It’s a highly specific optimization though, so I wonder if such a thing has been implemented in the Rust compiler. Anybody who has an idea about this?

  • Gobbel2000@programming.dev
    link
    fedilink
    arrow-up
    12
    ·
    7 months ago

    This blog post goes into some specifics of Rust reusing Vec allocations and some of the consequences. I think it’s really worth a read to better understand Vecs. From what I understand, it is possible that Rust will reuse the allocation of vec_a in your case, but it ultimately is quite complicated.

    • arendjr@programming.devOP
      link
      fedilink
      arrow-up
      2
      ·
      7 months ago

      That was a super interesting and informative read! Exactly what I was hoping to find when I posted this, thanks!

  • Vorpal@programming.dev
    link
    fedilink
    arrow-up
    3
    ·
    7 months ago

    The standard library does have some specialisation internally for certain iterators and collection combinations. Not sure if it will optimise that one specifically, but Vec::into_iter().collect::<Vec>() is optimised (it may look silly, but it comes up with functions returning impl Iterator

    • arendjr@programming.devOP
      link
      fedilink
      arrow-up
      1
      ·
      7 months ago

      I mean, the actual operation is just an example, of course. Feel free to make it a .map() operation instead. The strings couldn’t be reused then, but the vector’s allocation still could… in theory.

        • arendjr@programming.devOP
          link
          fedilink
          arrow-up
          0
          ·
          edit-2
          7 months ago

          Yeah, that’s helpful if I would be currently optimizing a hot loop now. But I was really just using it as an example. Also, retain_mut() doesn’t compose as well.

          I’d much rather write:

          let vec_a: Vec<String> = /* ... */;
          let vec_b: Vec<String> = vec_a
              .into_iter()
              .filter(some_filter)
              .map(some_map_fn)
              .collect();
          

          Over:

          let mut vec_a: Vec<String> = /* ... */;
          vec_a.retain_mut(|x| if some_filter(x) {
              *x = some_map_fn(*x); // Yikes, cannot move out of reference.
              true
          } else {
              false
          });
          

          And it would be nice if that would be optimized the same. After all, the point of Rust’s iterators is to provide zero-cost abstractions. In my opinion, functions like retain_mut() represent a leakiness to that abstraction, because the alternative turns out to not be zero cost.

          • porgamrer@programming.dev
            link
            fedilink
            arrow-up
            1
            ·
            7 months ago

            Is it really fair to say retain doesn’t compose as well just because it requires reference-based update instead of move-based? I also think using move semantics for in-place updates makes it harder to optimise things like a single field being updated on a large struct.

            It also seems harsh to say iterators aren’t a zero-cost abstraction if they miss an optimisation that falls outside what the API promises. It’s natural to expect collect to allocate, no?

            But I’m only writing this because I wonder if I haven’t understood your point fully.

            (Side note: I think you could implement the API you want on top of retain_mut by using std::mem::replace with a default value, but you’d be hoping that the compiler optimises away all the replace calls when it inlines and sees the code can’t panic. Idk if that would actually work.)