fitzgen / bumpalo

A fast bump allocation arena for Rust
https://docs.rs/bumpalo
Apache License 2.0
1.41k stars 111 forks source link

Pinned Box #228

Open cynecx opened 8 months ago

cynecx commented 8 months ago

Fixes https://github.com/fitzgen/bumpalo/issues/226. Fixes https://github.com/fitzgen/bumpalo/issues/186.

This adds a new "pinned" Box type (pin::Box<T>), that guarantees running T's drop impl whenever the box is leaked and the Bump allocator is reset or dropped.

The impl utilizes a "intrusive circular doubly linked list" that links every "pinned" allocation that needs a cleanup. Each list entry detaches when the Box itself is dropped. That way, when the Bump allocator is cleared, we can run the Drop impls by traversing the list.

The pinned Box is still one pointer-sized big. We can calculate the each allocation's header pointer by doing some pointer arithmetic. Note that this only passes miri with tree-borrows enabled (not compatible with stacked borrows and reports UB) due to the kind of container-of style pointer arithmetic being used.

As I mentioned above this has the implication that drop impls have to run when the bump allocator is cleared which goes against bumpaloo's current situation that resets are "extremely fast" and don't run any Drop impls. So this change is a breaking change and needs discussion first. Whether or not we want to actually support this. One simple alternative would be to simply remove the support for pinned Boxes.

TODO

fitzgen commented 7 months ago

I don't have time to dig in yet, but here are some initial thoughts:

cynecx commented 7 months ago

I don't mind losing the reset-is-just-a-pointer-update property

👍

presumably if the user doesn't allocate any pinned boxes in the arena, then it doesn't impose any overhead other...

Yes, that's how it's currently implemented. (Besides, the sentinel node is also currently lazily allocated and initialized.)

I have vague plans for adding sub-arenas/checkpoints in the next breaking release of bumpalo...

Exciting stuff!

Is this ordering requirement problematic? I don't think it should be, but I also haven't looked into the details yet.

Yeah, I don't think the ordering requirement is going to be a problem. The current impl uses a doubly linked list, so you could just iterate forwards or backwards regardless in what order you push the drop entries.

However, the way some APIs are currently implemented might pose an issue here:

Both pin::Box<T>::into_raw and pin::Box<T>::from_raw unlink/re-add the drop entry when called. Therefore, invalidating the ordering invariant that we'd possibly want to preserve. Perhaps the question is rather whether these APIs should've done that in the first place. Or perhaps we should just keep the drop entry attached, and store a is_detached: bool in the header and toggle that when into_raw/from_raw is called.

Also, I am not exactly sure about the design you have in mind but I can also imagine one could maintain multiple drop lists for each sub-arena/checkpoint, but that kind of depends on how it's actually implemented...

It would be very good to have extensive quickcheck tests for this stuff.

I've added some basic tests that test the API surface of the pinned Box. But we should probably add some tests around the internals (drop list) too.

Is this fundamental to the intrusive list design or an implementation detail?

It's more of an implementation detail (See below).

Would it help to hold a pointer to the header inside the box and then access the data as an offset of the header, rather than the other way around?

That won't work with SB because it's still doing "pointer arithmetic" (playground). IIUC, pointers can only be derived by actually "reborrowing" from a pointer, aka, we'd have to repr the pinned Box like struct Box<T>(*const DropEntry<T>) and derive the data pointer with something like &(*self.0).data. However, that representation is somewhat wonky because unsized coercion doesn't work anymore, which is quite unfortunate because we can't have a Box<dyn Future> anymore.

Although, I admit I'm not an expert in the SB/Tree-Borrows pointer aliasing models, so I might be totally wrong about this 😅.