Open cynecx opened 8 months ago
I don't have time to dig in yet, but here are some initial thoughts:
I don't mind losing the reset-is-just-a-pointer-update property because (a) that isn't quite true already when there are multiple chunks and (b) presumably if the user doesn't allocate any pinned boxes in the arena, then it doesn't impose any overhead other than a single compare and branch on the empty intrusive list, which is fine. If the implementation doesn't reflect (b) then we should investigate ways to make it so.
I have vague plans for adding sub-arenas/checkpoints in the next breaking release of bumpalo (that said, nothing is concretely planned yet, and it might even never happen). This would take advantage of the LIFO nature of bump allocation to reset the bump pointer to a place in the middle of a chunk where the user made a checkpoint, keeping alive earlier allocations but freeing everything allocated since that checkpoint. To be compatible with this, the drop list would need to be stored in youngest-to-oldest allocation order so that we could keep dropping pinned boxes while they were past the checkpoint and stop as soon as we encounter one that is within the checkpoint. If the list was in the opposite order, resetting to a checkpoint would need to walk over every pinned box within the checkpoint before getting to the ones to drop that are outside the checkpoint, effectively making the operation O(total pin boxes) rather than O(pin boxes to drop). Is this ordering requirement problematic? I don't think it should be, but I also haven't looked into the details yet.
It would be very good to have extensive quickcheck tests for this stuff.
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.
Is this fundamental to the intrusive list design or an implementation detail? All else being equal it would be nice to support both models, but it wouldn't be the end of the world to support only tree borrows. 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? I don't know if that would better propagate provenance or whatever.
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 😅.
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 runningT
's drop impl whenever the box is leaked and theBump
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 theDrop
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