Implementing a pool of objects

Hi, I’m implementing a pool of objects in the heap. The idea is to avoid allocations and share those elements across threads by moving them around rather than allocating/releasing. My implementation looks something like:

#[derive(Debug)]
struct Element(u32);

#[derive(Debug)]
struct Pool {
  elements: Vec<Box<Element>>
}

impl Pool {
    pub fn alloc(&mut self) -> Option<Box<Element>> {
        self.elements.pop()
    }
    
    pub fn release(&mut self, element: Box<Element>) {
        self.elements.push(element);
    }
}

fn main() {
    let e1 = Box::new(Element(1));
    let e2 = Box::new(Element(2));
    let mut pool = Pool { elements: vec![e1, e2] };
    println!("{:?}", pool);
    let a1 = pool.alloc().unwrap();
    println!("{:?} -> {:?}", pool, a1);
    let a2 = pool.alloc().unwrap();
    println!("{:?} -> {:?}", pool, a2);
    pool.release(a2);
    println!("{:?}", pool);
    pool.release(a1);
    println!("{:?}", pool);
}

(Playground)

but a warning from Clippy called my attention:

   Checking playground v0.0.1 (/playground)
warning: `Vec<T>` is already on the heap, the boxing is unnecessary.
 --> src/main.rs:6:13
  |
6 |   elements: Vec<Box<Element>>
  |             ^^^^^^^^^^^^^^^^^ help: try: `Vec<Element>`
  |
  = note: #[warn(clippy::vec_box)] on by default
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#vec_box

    Finished dev [unoptimized + debuginfo] target(s) in 1.09s

which makes me feel suspicious of my Pool implementation.

Could someone help me understand the problem with my Pool implementation ? Are the semantics that I expect really well implemented ? I’d expect that the Vec contains pointers to elements in the heap, whenever an alloc is called that pointer is extracted from the Vec and the ownership transferred to someone else. Later on, when the work is done, the pointer is returned back to the Pool.

For more details, the original Pool implementation can be found here

Thank you very much.

I would expect the pool to be generic, and more like

#[derive(Debug)]
struct Pool<T> {
  elements: Vec<T>
}

impl<T> Pool<T> {
    pub fn alloc(&mut self) -> Option<T> { ... }
    pub fn release(&mut self, element: T) { ... }
}

Because then it’s up to the user of the pool whether the things inside the pool should themselves be boxed. I can pick if I want a Pool<Arc<Foo>> or a Pool<Box<[u8; 1600]>>` or whatever.

(Also, consider whether you want to give out smart handles that put things back into the pool on drop.)

You can ignore this warning.

Clippy wants to avoid a relatively costly double indirection (vec itself contains a pointer to the heap, so vec of boxes is a pointer to an array of pointers), but in your particular implementation that allows growing the storage and recycling arbitrary allocations, the double indirection is unavoidable.

However, be sure to benchmark this to check if it’s actually any faster than just using jemalloc. Jemalloc already has free lists for recycling objects of certain sizes, so in a way it has that kind of pool already built-in.

It is possible to make a pool that avoids the cost of double indirection, by pre-allocating space for multiple objects and giving out direct pointers to within that space. Vec doesn’t guarantee that directly, so the implementation is non-trivial. See https://lib.rs/mempool and https://lib.rs/slab

1 Like

Hi @scottmcm, my actual implementation is generic on T, but it fixes the Box because I am using it in a very specific scenario (not building a generic lib). My purpose is that I want to store and move pointers to the heap across threads, rather than the data itself. But the warning from clippy made me think that if Vec<Box<T>> is the same as Vec<T>, then my Pool is moving data rather than pointers to the heap.

For the thread support, Rust will also force you to add Mutex (or some other synchronization) around your Vec, so you will have a cross-thread bottleneck there.

This may backfire terribly, unless you’re recycling much more than just memory allocation. In general-purpose allocators such thread cross-talk has been a big performance bottleneck, and all the fast allocators stopped cross-thread sharing in favor of thread-local storage.

@kornel thanks for the clarification, that relieves me a bit. I will take a deeper look into the libs you suggested.

I’m building an audio application, where there can not be allocations nor locks within the real-time threads. So I initialise a pool on the master thread, which allocates the buffers whenever it needs to produce some audio chunk, and passes it to another one through a crossbeam channel. That other thread sends the audio to some device and returns the buffer back through another crossbeam channel. Then the master thread can put it back to the pool and re-use it later on.

I have something working based on that idea without using Mutex nor Arc, but wanted to verify that I was moving pointers and not the data itself. (there are some faults in my current implementation of the idea, like having the get_or_alloc, or not using the right crossbeam channel, but they are for another story :wink:

Thanks, your response was really helpful.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.