Skip to content

Remove allocation for ZSTs - #92

Open
Baptistemontan wants to merge 9 commits into
mozilla:mainfrom
Baptistemontan:no_alloc_zst
Open

Baptistemontan wants to merge 9 commits into
mozilla:mainfrom
Baptistemontan:no_alloc_zst

Conversation

@Baptistemontan

Copy link
Copy Markdown

Store the length as a NonZero<usize> inside the NonNull<Header>, the pointer is actually never read for ZSTs, when one is needed just create a dangling one.
The only problem is that now the length is off by one (as it can't be 0), this makes the maximum capacity usize::MAX - 1 instead, this might be a breaking change ?
Did'nt enabled those optimizations for feature = "gecko-ffi", as it can't be turned into nsTArray, tho from my understanding ZSTs are undefined in C++ so it would be UB to turn a ThinVec of ZSTs into a nsTArray anyway, but I prefer to get confirmation first

@emilio emilio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, can you elaborate on what's the rationale for this? It seems somewhat complicated and having an array of ZSTs is already pretty edgecasey.

In general, I'd rather keep the differences between the gecko-ffi path and the other as small as possible.

@Baptistemontan

Baptistemontan commented Sep 1, 2026

Copy link
Copy Markdown
Author

Deliberately constructing a Vec of ZSTs is very niche, but it's actually pretty common when generics comes into play, deeply nested in data structures. This lib might be a bridge for Gecko but it's used a lot outside of that space by other people, and removing allocations for ZSTs is basically free.
Also, the crate doc says

ThinVec currently doesn’t bother to not-allocate for Zero Sized Types (e.g. ThinVec<()>), but it could be done if someone cared enough to implement it.

Well I'm that someone lol

And for keeping the same paths, it could be enabled on both since ZSTs should'nt be passed to C++ anyway

Comment thread src/lib.rs
Comment thread src/lib.rs
Comment thread src/lib.rs
unsafe fn reallocate(&mut self, new_cap: usize) {
debug_assert!(new_cap > 0);
debug_assert!(!Self::is_zst());
if self.has_allocation() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_allocation still returns true for ZSTs, which seems rather footgunny.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally footgunny, I'm gonna update has_allocation.
But in the case of reallocate even if has_allocation return false it still take a bad path in case T is ZST.

Comment thread src/lib.rs Outdated
Comment on lines +2049 to +2051
#[cold]
#[inline(never)]
fn drop_zsts<T>(this: &mut ThinVec<T>) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this cold? is_singleton is false for zst so aren't you forcing a function call for no reason?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I was probably just following drop_non_singleton, but yeah you're correct, unlike singletons ZSTs don't have a branch, I should just put the drop in place inside the drop function

@Baptistemontan

Copy link
Copy Markdown
Author

Humm, tests are going to fail for "const_new", I can't make len_to_ptr_unchecked const with msrv 1.53, but const new already requires 1.83, I would like to have the const_new version of len_to_ptr_unchecked to use NonNull::without_provenance instead of transmute shenanegans but it's const stable only for 1.89, so for now I'll just duplicate the function and slap const under a feature cfg.

@emilio

emilio commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

I think we should just bump the MSRV, see #93. Can you rebase atop?

@Baptistemontan

Copy link
Copy Markdown
Author

@emilio done

@emilio emilio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, mostly nits, looks good with this!

Comment thread src/lib.rs Outdated
}
} else {
// See the comment in with_capacity().
let _ = padding::<T>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to disallow ZSTs in gecko-ffi mode altogether, see #94. I think you need to keep calling padding unconditionally.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure !

Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
if old_len == self.capacity() {
if Self::is_zst() {
assert!(old_len < MAX_CAP);
} else if old_len == self.capacity() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be cleaner like this:

if old_len == self.capacity() {
    if Self::is_zst() {
        capacity_overflow();
    }
    self.reserve(1);
}

That gives consistent panic messages regardless of ZST.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Comment thread src/lib.rs Outdated
self.set_len_non_singleton(old_len - 1);
Some(ptr::read(self.data_raw().add(old_len - 1)))
if Self::is_zst() {
Some(mem::zeroed())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if this matches what Vec or so does? It seems ok-ish to me but worth double-checking.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just checked, and Vec actually don't branch on ZSTs and just read from the pointer, I'll do that then, and my guess is that it's going to compile to the same thing as a pointer read of ZSTs are noop anyway

Comment thread src/lib.rs Outdated

assert!(idx <= old_len, "Index out of bounds");
if Self::is_zst() {
assert!(old_len < MAX_CAP);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark about reserve() and the panic message. In fact maybe reserve() should just have a if Self::is_zst() { capacity_overflow(); }? Then the specialness can be reduced.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, and with the ocmment about pop I checked Vec for that too and they don't have special handling of ZSTs either for insert, just write the value to the offseted ptr, so I'll reduce the special handling there too

Comment thread src/lib.rs
return;
}
// only way to get here is if min_cap == usize::MAX, which we can't handle.
if Self::is_zst() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, there we go, so I think some of the special cases elsewhere aren't really needed.

Comment thread src/lib.rs

@Baptistemontan Baptistemontan left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright I removed a good chunk of unnecessary zst checks (either unneeded or checked afterward anyway).
I think this should wait for #94 to merge, there might be some conflicts as those two PRs both adresses ZSTs

Comment thread src/lib.rs Outdated
}
} else {
// See the comment in with_capacity().
let _ = padding::<T>();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure !

Comment thread src/lib.rs Outdated
if old_len == self.capacity() {
if Self::is_zst() {
assert!(old_len < MAX_CAP);
} else if old_len == self.capacity() {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Comment thread src/lib.rs Outdated
self.set_len_non_singleton(old_len - 1);
Some(ptr::read(self.data_raw().add(old_len - 1)))
if Self::is_zst() {
Some(mem::zeroed())

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just checked, and Vec actually don't branch on ZSTs and just read from the pointer, I'll do that then, and my guess is that it's going to compile to the same thing as a pointer read of ZSTs are noop anyway

Comment thread src/lib.rs Outdated

assert!(idx <= old_len, "Index out of bounds");
if Self::is_zst() {
assert!(old_len < MAX_CAP);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, and with the ocmment about pop I checked Vec for that too and they don't have special handling of ZSTs either for insert, just write the value to the offseted ptr, so I'll reduce the special handling there too

Comment thread src/lib.rs

@emilio emilio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last pass, I promise. Fairly minor things :)

Comment thread src/lib.rs
/// len must be != 0, this uses the `NonNull` to store a length, so the length must be stored offset by one.
/// This function expect the len to be already shifted
#[inline(always)]
const unsafe fn len_to_ptr_unchecked<T: Sized>(len: usize) -> NonNull<T> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a debug_assert!(len != 0); at least.

Comment thread src/lib.rs
// `Drop` impl, trippng an assertion along that code path causes a
// double panic. We duplicate the assertion here so that it is
// testable,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random newline?

Comment thread src/lib.rs
///
/// // Only true **without** the gecko-ffi feature!
/// // assert_eq!(vec_units.capacity(), usize::MAX);
/// // assert_eq!(vec_units.capacity(), usize::MAX - 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I enabled this in the other PR so will need a trivial-ish rebase.

Comment thread src/lib.rs
debug_assert!(
len <= MAX_CAP,
"invalid set_len(usize::MAX) on ZST ThinVec (max cap is usize::MAX - 1)"
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this assert isn't super useful, because len + 1 would debug-assert anyway. But fine to keep.

Comment thread src/lib.rs
fn is_singleton(&self) -> bool {
unsafe { self.ptr.as_ptr() as *const Header == &EMPTY_HEADER }
// could technicaly remove this branch
// but there is a 1/2^64 chance of the number of ZST being equal to &EMPTY_HEADER

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove this comment, as given that chance you can't remove the branch, right? :)

Comment thread src/lib.rs
/// # Safety
///
/// This function drop and deallocates the inner values of the `ThinVec`,
/// invariants are therefore brokens and the value must be considered dropped and should not be accessed again.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: broken.

Comment thread src/lib.rs
impl<T> MallocShallowSizeOf for ThinVec<T> {
fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
if self.capacity() == 0 || self.uses_stack_allocated_buffer() {
if self.capacity() == 0 || self.uses_stack_allocated_buffer() || Self::is_zst() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified to if !self.has_allocation() {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants