0

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

Tomáš Dvořák
  • 1,490
  • 1
  • 9
  • 20
  • 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 Jun 11 '20 at 18:26
  • It's hard to answer your question because it doesn't include a [MRE]. 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](https://play.rust-lang.org) if possible, otherwise in a brand new Cargo project, then [edit] your question to include the additional info. There are [Rust-specific MRE tips](//stackoverflow.com/tags/rust/info) you can use to reduce your original code for posting here. Thanks! – Shepmaster Jun 11 '20 at 18:26
  • 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?](https://stackoverflow.com/q/32300132/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Jun 11 '20 at 18:27
  • 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 Jun 12 '20 at 06:58

1 Answers1

2

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.

Brent Kerby
  • 1,397
  • 8
  • 14
  • Yes, *"is it possible to recycle the allocated memory from the old Vec in order to skip having to allocate the next Vec"* sums the crux of the question nicely. I was thinking along in the lines of wrangling not `Vec`, but perhaps just a `Vec>>` that would be transmuted to match the lifetime. Perhaps that wouldn't be *that* magical, would it? As for performance, it is not bottleneck for my particular app, but the actual data processing with `process_fn` is often just a simple addition, and unnecessary allocation in the same loop looks wasteful. – Tomáš Dvořák Jun 14 '20 at 10:03
  • Yes, transmuting from and to `Vec>>` should work; that's a lot cleaner than using `Vec`. The main risk here is that this undermines the safety mechanism built into the guard, in that if we mess up we could easily end up with guards sticking around longer than they should; i.e., we could end up with multiple write guards coexisting, or with write guards coexisting with read guards. But if you make sure that the vector is always empty at the time of transmute, then I guess you are safe. – Brent Kerby Jun 14 '20 at 16:04
  • Still, I wonder if an alternative way of structuring the data might give you better performance and also eliminate the need for tricks like this. If your `process_ts` does very little work, then I would guess that the `RwLock::read` calls are going to be the bottleneck. There might be a way to do what you're trying to do without using locks at all. For instance, the crate `bus` (https://docs.rs/bus/2.2.3/bus/) implements a lock-free single-producer, multiple-consumer, broadcast channel, which might be a relevant building block. – Brent Kerby Jun 14 '20 at 16:37
  • I've significantly reworded the question to your suggestions. I was thinking a lot about alternative approaches, but I'm not only processing the data as it comes, I also sometimes process it in random order, or even mutate it, so this solution OK for my use case, I guess. In reality I use `parking_lot::RwLock` and the lock is not contended, so the locking itself should be pretty fast. I'll be thinking about transmuting the lifetimes, but for the time being I'll probably just allocate the `Vec` each time I need it. Thanks a lot for your very valuable feedback, though. – Tomáš Dvořák Jun 15 '20 at 11:49
  • @TomášDvořák FWIW, transmuting the lifetimes is probably what I would do (if I determined `unsafe` code was appropriate). `Vec` poses a problem because `u8` and `RwLockReadGuard<'_, _>` have different layouts, so it becomes *super* tricky to create the `Vec`, be sure it is always grown and shrunk by the right amounts, and drop it when it goes out of scope without breaking stuff. (As opposed to transmuting the lifetimes and being sure that you can never leak a long-lived reference to a short-lived thing, which is just *regular* tricky.) – trent Jun 15 '20 at 11:59