I've been working on second_stack2
, a library which manages a thread local second stack on the heap. It can be used to store temporary slices and large values, and when trying to replace recursion with a loop. It is heavily inspired by second_stack
but tries to be more efficient by only using const initialized, trivially droppable thread locals on the fast path as well as having a more flexible API.
The key type of this library is StackVec<'a, T>
which acts as a typed handle to push and pop elements to the second stack. A StackVec<'a, T>
can be converted into a StackBox
<'a, [T]>
(a library version of &'a own [T]
/&'a move [T]
). When the heap allocation for the second stack is full, a new allocation of twice the size is allocated and all of the contents are copied over, but the old allocation isn't freed until the thread terminates. This is necessary since any StackBox
s that were created will still be referencing the first allocation. Existing StackVec
s will switched to using the new allocation so the memory does need to be copied. Unfortunately this does lead to a stacked borrows violation, although my current test suite does pass under tree borrows.
Example of stacked borrows violation:
// LEN: 0, CAP: 0, ALLOCATION#0
reserve_buffer_capacity(16);
// LEN: 0, CAP: 16, ALLOCATION#1
with_stack_vec(|mut v1: StackVec<Box<bool>>| {
// LEN: 0, CAP: 16, ALLOCATION#1
v1.push(Box::new(true));
// LEN: 8, CAP: 16, ALLOCATION#1
with_stack_vec(|mut v2: StackVec<Box<char>>| {
// LEN: 8, CAP: 16, ALLOCATION#1
v2.push(Box::new('🦀'));
// LEN: 16, CAP: 16, ALLOCATION#1
let mut s2: StackBox<[Box<char>]> = v2.into_slice();
let s2: &mut [Box<char>] = &mut *s2;
// s2 is pointing in to ALLOCATION#1
with_stack_vec(|mut v3: StackVec<u8>| {
// LEN: 16, CAP: 16, ALLOCATION#1
v3.push(0);
// LEN: 17, CAP: 32, ALLOCATION#2
});
// LEN: 16, CAP: 32, ALLOCATION#2
assert_eq!(*s2[0], '🦀')
// ALLOCATION#1 is still exists but stacked borrows complains the box has been moved out
});
// LEN: 8, CAP: 32, ALLOCATION#2
v1.push(Box::new(false));
// LEN: 8, CAP: 32, ALLOCATION#2
let mut s1: StackBox<[Box<bool>]> = v1.into_slice();
let s1: &mut [Box<bool>] = &mut *s1;
// s1 is pointing in to ALLOCATION#2 since this is where the second box was allocated
// it relies on the first box being copied from ALLOCATION#1 to ALLOCATION#2
assert_eq!((*s1[0], *s1[1]), (true, false));
})
// LEN: 0, CAP: 32, ALLOCATION#2
In case your worried about how alternating pushes from two StackVec
s are handled, the function passed to with_stack_vec
has a Send
bound, and StackVec
doesn't implement Send
so having two StackVec
s at the same time should be impossible. Ideal the Send
bound could be relaxed to a custom auto trait
that is explicitly not implemented for StackVec
, but hopefully the Send
bound is a good enough compromise to allow things to work on stable. Theoretically if the auto trait
was used and multiple copies of second_stack2
existed using both at the same time should still be sound since the two crates would use different heap allocated stacks.
I would appreciate if someone would look it over to see if I am missing any potential unsoundness.