Use ManuallyDrop in drop()

For some reason I don't want my struct to be collected when its lifetime is over, instead, I want to save it for later use, here's my solution:

struct Foo {
    data: Vec<u8>,
}

impl Foo {
    thread_local! {
        static CACHE: UnsafeCell<Vec<Foo>> = UnsafeCell::new(Vec::new());
    }
}

impl Drop for Foo {
    fn drop(&mut self) {
        self.data.clear();
        let foo_default = Foo { data: Vec::new() };
        let foo = std::mem::replace(self, foo_default);
        Self::CACHE.with(|cache| {
            unsafe { (*cache.get()).push(foo) }
        });
    }
}

But the code is not the most efficiency, even though Vec::new do not allocate memory, let foo_default = Foo { data: Vec::new() }; is not zero cost, which I don't really need. But I can't use MaybeUninit, since the replaced data field will be collect.

I tried to use MauallyDrop, but it takes a Foo, while drop only offer a &mut Foo.

So how to write to make this drop function more efficiency?

P.S. the let mut vec = cache.take(); has the same problem

You should use ManuallyDrop in the field:

struct Foo {
    data: ManuallyDrop<Vec<u8>>,
}

But don't bother. The overhead is so small I don't believe it will affect something,

BTW, you can #[derive(Default)] then instead of std::mem::replace(self, Foo { ... }) just do std::mem::take(self).

2 Likes

I considered this solution, but it just changes things too much, the refactor will take times :rofl:

It is zero cost. Why not?

I think it will write some default value to memory, some sort of memset(0, sizeof(foo_default) . So I guess it's more costy than use MaybeUninit::asssume_init. Or do I miss something?

If the value is dropped immediately after, LLVM may optimize the write away.

It will create a triple like (1usize, 0usize, 0usize), i.e. "dangling pointer to u8, capacity = 0, length = 0". This is not literally zero-cost, but any other operation around here will be more costly.

1 Like

When in doubt, try to see if a helper const can be used:

const DEFAULT_FOO: Foo = Foo { data: Vec::new() };

then, by design, producing a CONST is guaranteed to be zero-cost (modulo the bitwise-copy of the entity itself).

  • In this instance, it just so happens that Vec::new() itself is designed to be zero-cost as well; it's basically the same as producing a (1, 0, 0) triplet of usize values.

With it, doing mem::replace(self, DEFAULT_FOO); is thus guaranteed to be as zero-cost as possible: bit-copying the vec and potentially[1] "replacing it with a "NULL vec"[2].


  1. with optimizations this will probably be skipped altogether ↩︎

  2. this is what Vec::new() yields, for what is worth ↩︎

2 Likes

I think it maybe a better way to write a custom function that takes the ownership of Foo, do what you want, then panic when manually drop by setting a special field.

In addition, if you want to collect the instance that going to be dropped by implementing Drop for Foo, you'll face a situation that how to drop collected instance. Memory leak, stack overflow or dead cycle will be the result.

1 Like

That's a good point, using drop like this is definitely an ill design, I think I should refactor the code after all, though it is painful.