Steve Pryde
Steve Pryde

Reputation: 93

How do I mutate and optionally remove elements from a vec without memory allocation?

I have a Player struct that contains a vec of Effect instances. I want to iterate over this vec, decrease the remaining time for each Effect, and then remove any effects whose remaining time reaches zero. So far so good. However, for any effect removed, I also want to pass it to Player's undo_effect() method, before destroying the effect instance.

This is part of a game loop, so I want to do this without any additional memory allocation if possible.

I've tried using a simple for loop and also iterators, drain, retain, and filter, but I keep running into issues where self (the Player) would be mutably borrowed more than once, because modifying self.effects requires a mutable borrow, as does the undo_effect() method. The drain_filter() in nightly looks useful here but it was first proposed in 2017 so not holding my breath on that one.

One approach that did compile (see below), was to use two vectors and alternate between them on each frame. Elements are pop()'ed from vec 1 and either push()'ed to vec 2 or passed to undo_effect() as appropriate. On the next game loop iteration, the direction is reversed. Since each vec will not shrink, the only allocations will be if they grow larger than before. I started abstracting this as its own struct but want to check if there is a better (or easier) way.

This one won't compile. The self.undo_effect() call would borrow self as mutable twice.

struct Player {
    effects: Vec<Effect>
}

impl Player {
    fn update(&mut self, delta_time: f32) {
        for effect in &mut self.effects {
            effect.remaining -= delta_time;
            if effect.remaining <= 0.0 {
                effect.active = false;
            }
        }

        for effect in self.effects.iter_mut().filter(|e| !e.active) {
            self.undo_effect(effect);
        }

        self.effects.retain(|e| e.active);
    }
}

The below compiles ok - but is there a better way?

struct Player {
    effects: [Vec<Effect>; 2],
    index: usize
}

impl Player {
    fn update(&mut self, delta_time: f32) {
        let src_index = self.index;
        let target_index = if self.index == 0 { 1 } else { 0 };

        self.effects[target_index].clear(); // should be unnecessary.
        while !self.effects[src_index].is_empty() {
            if let Some(x) = self.effects[src_index].pop() {
                if x.active {
                    self.effects[target_index].push(x);
                } else {
                    self.undo_effect(&x);
                }
            }
        }

        self.index = target_index;
    }
}

Is there an iterator version that works without unnecessary memory allocations? I'd be ok with allocating memory only for the removed elements, since this will be much rarer.

Would an iterator be more efficient than the pop()/push() version?

EDIT 2020-02-23: I ended up coming back to this and I found a slightly more robust solution, similar to the above but without the danger of requiring a target_index field.

std::mem::swap(&mut self.effects, &mut self.effects_cache);
self.effects.clear();
while !self.effects_cache.is_empty() {
    if let Some(x) = self.effects_cache.pop() {
        if x.active {
            self.effects.push(x);
        } else {
            self.undo_effect(&x);
        }
    }
}

Since self.effects_cache is unused outside this method and does not require self.effects_cache to have any particular value beforehand, the rest of the code can simply use self.effects and it will always be current.

Upvotes: 0

Views: 3211

Answers (3)

Robert Jacobson
Robert Jacobson

Reputation: 141

The best answer is this one in C++.

[O]rder the indices vector, create two iterators into the data vector, one for reading and one for writing. Initialize the writing iterator to the first element to be removed, and the reading iterator to one beyond that one. Then in each step of the loop increment the iterators to the next value (writing) and next value not to be skipped (reading) and copy/move the elements. At the end of the loop call erase to discard the elements beyond the last written to position.

The Rust adaptation to your specific problem is to move the removed items out of the vector instead of just writing over them.

An alternative is to use a linked list instead of a vector to hold your Effect instances.

Upvotes: 0

Matthieu M.
Matthieu M.

Reputation: 299810

The main issue is that you are borrowing a field (effects) of Player and trying to call undo_effect while this field is borrowed. As you noted, this does not work.

You already realized that you could juggle two vectors, but you could actually only juggle one (permanent) vector:

struct Player {
    effects: Vec<Effect>
}

impl Player {
    fn update(&mut self, delta_time: f32) {
        for effect in &mut self.effects {
            effect.remaining -= delta_time;
            if effect.remaining <= 0.0 {
                effect.active = false;
            }
        }

        //  Temporarily remove effects from Player.
        let mut effects = std::mem::replace(&mut self.effects, vec!());

        //  Call Player::undo_effects (no outstanding borrows).
        //  `drain_filter` could also be used, for better efficiency.
        for effect in effects.iter_mut().filter(|e| !e.active) {
            self.undo_effect(effect);
        }

        //  Restore effects
        self.effects = effects;

        self.effects.retain(|e| e.active);
    }
}

This will not allocate because the default constructor of Vec does not allocate.

On the other hand, the double-vector solution might be more efficient as it allows a single pass over self.effects rather than two. YMMV.

Upvotes: 1

hellow
hellow

Reputation: 13430

If I understand you correctly, you have two questions:

  1. How can I split a Vec into two Vecs (one which fulfill a predidate, the other one which doesn't)
  2. Is it possible to do without memory overhead

There are multiple ways of splitting a Vec into two (or more).

  • You could use Iteratator::partition which will give you two distinct Iterators which can be used further.
  • There is the unstable Vec::drain_filter function which does the same but on a Vec itself
  • Use splitn (or splitn_mut) which will split your Vec/slice into n (2 in your case) Iterators

Depending on what you want to do, all solutions are applicable and good to use.


Is it possible without memory overhead? Not with the solutions above, because you need to create a second Vec which can hold the filtered items. But there is a solution, namely you can "sort" the Vec where the first half will contain all the items that fulfill the predicate (e.g. are not expired) and the second half that will fail the predicate (are expired). You just need to count the amount of items that fulfill the predicate.

Then you can use split_at (or split_at_mut) to split the Vec/slice into two distinct slices. Afterwards you can resize the Vec to the length of the good items and the other ones will be dropped.

Upvotes: 1

Related Questions