reserve()
The goal of reserve(n)
is simple: allocate enough memory to perform n
pushes
without allocating more memory.
This is a useful function, because, as we've seen, allocate_bucket
requires
some heavy atomics with compare_exchange
. If we can do our allocations in a
calmer scenario with less contention, we'll experience some performance gains.
We start by calculating the number of allocations we'll need to perform to
reserve enough space. The calculation is a little funky, and there's an edge
case where it can't distinguish between 0 and sizes between 1 and
FIRST_BUCKET_SIZE
. That's why we need to explicitly allocate the first bucket.
We'll see the implementation of size()
later, but it does use some atomic
synchronization, so we just cache the result so we can keep using it later
without calling size
again.
#![allow(unused)] fn main() { pub fn reserve(&self, size: usize) { // Cache the size to prevent another atomic op from due to calling `size()` again let current_size = self.size(); if current_size == 0 { self.allocate_bucket(0); } }
Now, we calculate the number of allocations we've made.
highest_bit
returns the highest set bit in a number. A bit is set
if it's equal to one. The highest set bit of 7 (0b111
), for example, is 2
(0-indexed). Since the buckets are increasing by a factor of two each time, the
highest set bit of the indices in each bucket is one greater than the highest
set bit of the indices in the previous bucket. Therefore, by using the highest
bit of a number in conjunction with FIRST_BUCKET_SIZE
, we can figure out how
many allocations are needed for a certain capacity. I know I'm waving my hands a
little; I haven't taken the time to rigorously understand the arithmetic, as
it's not that interesting to me, and in practice it works.
#![allow(unused)] fn main() { let mut num_current_allocs = highest_bit(current_size.saturating_add(FIRST_BUCKET_SIZE) - 1) .saturating_sub(highest_bit(FIRST_BUCKET_SIZE)); }
Then we calculate the number of allocations we need to reserve the space, and for each allocation missing, we allocate.
#![allow(unused)] fn main() { // Compare with the number of allocations needed for size `new` while num_current_allocs < highest_bit(size.saturating_add(FIRST_BUCKET_SIZE) - 1) .saturating_sub(highest_bit(FIRST_BUCKET_SIZE)) { num_current_allocs += 1; self.allocate_bucket(num_current_allocs as usize); } } }
And that's it for memory. We can now do every thing we need to do to access and top up the vector's memory. Now's time for the really hard part: actually implementing the vector's functions.
Complete source for reserve()
#![allow(unused)] fn main() { pub fn reserve(&self, size: usize) { // Cache the size to prevent another atomic op from due to calling `size()` again let current_size = self.size(); if current_size == 0 { self.allocate_bucket(0); } // Number of allocations needed for current size let mut num_current_allocs = highest_bit(current_size.saturating_add(FIRST_BUCKET_SIZE) - 1) .saturating_sub(highest_bit(FIRST_BUCKET_SIZE)); // Compare with the number of allocations needed for size `new` while num_current_allocs < highest_bit(size.saturating_add(FIRST_BUCKET_SIZE) - 1) .saturating_sub(highest_bit(FIRST_BUCKET_SIZE)) { num_current_allocs += 1; self.allocate_bucket(num_current_allocs as usize); } } }