Skip to content

Remove allocation for ZSTs - #92

Merged
emilio merged 10 commits into
mozilla:mainfrom
Baptistemontan:no_alloc_zst
Sep 16, 2026
Merged

emilio merged 10 commits into
mozilla:mainfrom
Baptistemontan:no_alloc_zst

Conversation

@Baptistemontan

Copy link
Copy Markdown
Contributor

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
Contributor 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
Comment thread src/lib.rs Outdated
@Baptistemontan

Copy link
Copy Markdown
Contributor 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
Contributor 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
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs
Comment thread src/lib.rs

@Baptistemontan Baptistemontan left a comment

Copy link
Copy Markdown
Contributor 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
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

technically already checked by NonZero::new_unchecked but it would probably help with backtrace and stronger code documentation

Comment thread src/lib.rs Outdated
// `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 Outdated
///
/// // 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no sure what you mean by that

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 Outdated
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? :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yep right

Comment thread src/lib.rs Outdated
/// # 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 Outdated
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() {

@emilio
emilio merged commit 696791e into mozilla:main Sep 16, 2026
5 checks passed
@emilio

emilio commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

FWIW I don't think that the MAX_CAP is a breaking chance in practice, IIUC the slice array size is capped at isize::MAX:

The isize type is a signed two’s complement integer type with the same number of bits as the platform’s pointer type. The theoretical upper bound on object and array size is the maximum isize value. This ensures that isize can be used to calculate differences between pointers into an object or array and can address every byte within an object along with one byte past the end.

https://doc.rust-lang.org/stable/reference/types/numeric.html#r-type.numeric.int.size.isize

@emilio

emilio commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the fix and for bearing with me btw! :)

Comment thread src/lib.rs
Comment on lines 578 to 581
/// **NOTE**: unlike `Vec`, `ThinVec` **MUST** allocate once to keep track of non-zero
/// lengths. As such, we cannot provide the same guarantees about ThinVecs
/// of ZSTs not allocating. However the allocation never needs to be resized
/// to add more ZSTs, since the underlying array is still length 0.

@Baptistemontan Baptistemontan Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@emilio not sure what to do with this paragraph, now that ZSTs are simply not allowed with gecko-ffi, and don't allocate without the feature the whole thing is wrong.
what about

   /// **NOTE**: like `Vec`, `ThinVec` does'nt allocate for ZSTs and store the length inline, 
   /// but creating a `ThinVec` of ZSTs is not allowed if the "gecko-ffi" feature is enabled.

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.

That looks about right!

@Baptistemontan

Copy link
Copy Markdown
Contributor Author

Thanks for the fix and for bearing with me btw! :)

No problem !
Thanks for your time

@Baptistemontan

Copy link
Copy Markdown
Contributor Author

ahem, there's still some docs I was updating lol

@emilio

emilio commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Err, sorry! Can you send another PR for those?

@Baptistemontan

Copy link
Copy Markdown
Contributor Author

Err, sorry! Can you send another PR for those?

sure, give me a moment

@Baptistemontan
Baptistemontan deleted the no_alloc_zst branch September 16, 2026 14:59
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