0
votes

My algorithm uses a Vec<RwLockReadGuard<..>> for processing data. The algorithm is called repeatedly, and I don't want to allocate the Vec each time it gets called, I'd be happy if I could just clear() it at the end of processing, and reuse it the next time by storing it in the same struct that is tied to the data processing. However, RwLockReadGuard takes a lifetime that is shorter than possible lifetime of the holding struct. Since I only use the Vec inside the data processing function, and outside it it is always empty, can I still store it in the struct somehow? Is there a crate or an idiom that might help me with that?

The minimal reproducible example showing the problem is at the bottom of the question.

This is how it would look like if I was allocating the Vec each time:

#[derive(Clone)]
pub struct T;

pub fn process_ts(ts: &[T]) {
    unimplemented!();
}

struct Process {
    locked_ts: Vec<RwLock<Vec<T>>>,
}

impl Process {
    pub fn process(&self) {
        let mut ts: Vec<T> = Vec::with_capacity(self.locked_ts.len());

        let guards: Vec<RwLockReadGuard<Vec<T>>> = self
            .locked_ts
            .iter()
            .map(|locked_t| locked_t.read().unwrap())
            .collect();

        let n = guards.iter().map(|guard| guard.len()).min().unwrap();

        for i in 0..n {
            ts.clear();
            for t in &guards {
                ts.push(t[i].clone());
                process_ts(&ts);
            }
        }
    }
}

What I don't like about this solution is that each time Process::process is called, ts: Vec<T> and guards: Vec<RwLockReadGuard<Vec<T>>> is allocated. I can get rid of ts:

struct ProcessReuseTs {
    locked_ts: Vec<RwLock<Vec<T>>>,
    reusable_ts: Vec<T>,
}

impl ProcessReuseTs {
    pub fn process(&mut self) {
        let guards: Vec<RwLockReadGuard<Vec<T>>> = self
            .locked_ts
            .iter()
            .map(|locked_t| locked_t.read().unwrap())
            .collect();

        let n = guards.iter().map(|guard| guard.len()).min().unwrap();

        for i in 0..n {
            self.reusable_ts.clear();
            for t in &guards {
                self.reusable_ts.push(t[i].clone());
                process_ts(&self.reusable_ts);
            }
        }
    }
}

But how can I extract guards?

use std::sync::{RwLock, RwLockReadGuard};

#[derive(Clone)]
pub struct T;

pub fn process_ts(ts: &[T]) {
    unimplemented!();
}

struct ProcessReuseBoth {
    locked_ts: Vec<RwLock<Vec<T>>>,
    reusable_ts: Vec<T>,
    reusable_guards: Vec<RwLockReadGuard<Vec<T>>>,
}

impl ProcessReuseBoth {
    pub fn process(&mut self) {
        self.reusable_guards.clear();
        self.reusable_guards.extend(
            self.locked_ts
                .iter()
                .map(|locked_t| locked_t.read().unwrap()),
        );

        let n = self
            .reusable_guards
            .iter()
            .map(|guard| guard.len())
            .min()
            .unwrap();

        for i in 0..n {
            self.reusable_ts.clear();
            for t in &self.reusable_guards {
                self.reusable_ts.push(t[i].clone());
                process_ts(&self.reusable_ts);
            }
        }

        self.reusable_guards.clear();
    }
}

pub fn main() {
    unimplemented!()
}

doesn't compile with

error[E0106]: missing lifetime specifier
  --> src/main.rs:13:26
   |
13 |     reusable_guards: Vec<RwLockReadGuard<Vec<T>>>,
   |        

Playground

1
Please edit your question and paste the exact and entire error that you're getting — that will help us to understand what the problem is so we can help best. Sometimes trying to interpret an error message is tricky and it's actually a different part of the error message that's important. Please use the message from running the compiler directly, not the message produced by an IDE, which might be trying to interpret the error for you. - Shepmaster
It's hard to answer your question because it doesn't include a minimal reproducible example. We can't tell what crates (and their versions), types, traits, fields, etc. are present in the code. It would make it easier for us to help you if you try to reproduce your error on the Rust Playground if possible, otherwise in a brand new Cargo project, then edit your question to include the additional info. There are Rust-specific MRE tips you can use to reduce your original code for posting here. Thanks! - Shepmaster
It looks like your question might be answered by the answers of Why can't I store a value and a reference to that value in the same struct?. If not, please edit your question to explain the differences. Otherwise, we can mark this question as already answered. - Shepmaster
I've added the minimal reproducible example and exact error. Your suggested answer is very broad and covers lots of topics, I can't see how it solves my problem (maybe it does, but I can't see how to apply it). I'd say the difference is that in my case the Vec of references is always empty outside the fn with an exclusive (&mut) access to the struct. It basically only serves as a pre-allocated area for temporary usage internal to the fn. Isn't there some clever way to exploit this property? - Tomáš Dvořák

1 Answers

2
votes

To boil it down, it seems that what you are trying to do is allocate one Vec. use it to store elements of types RwLockReadGuard<'a, Vec<T>> for some lifetime 'a, then clear the vector and put it elements of type RwLockReadGuard<'b, Vec<T>>, where lifetime 'b is a distinct lifetime from 'a (and in fact has no overlap with it), and so on. This can't work, because RwLockReadGuard<'a, Vec<T>> is a different type from RwLockReadGuard<'b, Vec<T>> and we can't change the type of elements that a Vec holds.

But maybe the real goal isn't to hold these different types of elements with the same Vec (which isn't possible) but rather to just avoid needing to reallocate each new Vec. We could ask, is it possible to recycle the allocated memory from the old Vec in order to skip having to allocate the next Vec? Well, with some very ugly, unsafe code, it might be possible to just allocate a Vec<u8> and then on each call of process do some pointer wrangling to convert it in-place into a Vec of the desired type (of zero size but non-zero capacity); this would probably be difficult to do correctly and would require depending on internal details of the Vec implementation in std.

It's maybe worth taking a step back and recognizing that we could ask the same question every time we allocate something on the heap -- namely, is there a way to reuse the space from something we just freed in order to avoid having to do a new allocation? In some cases, the answer might be yes, but then we have to ask, is it worth it to mess up our code in order to make that optimization?

This leads to the question -- do we have any evidence that the allocation here is actually a significant performance bottleneck? If not, maybe there's no need to worry about it. If you do need to improve the performance of allocations, you could try using jemalloc or some kind of arena.