Skip to content

Commit f84f565

Browse files
committed
Implement try_reserve
1 parent 24d71b5 commit f84f565

File tree

4 files changed

+175
-39
lines changed

4 files changed

+175
-39
lines changed

src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,13 @@ pub mod hash_set {
8080

8181
pub use map::HashMap;
8282
pub use set::HashSet;
83+
84+
/// Augments `AllocErr` with a CapacityOverflow variant.
85+
#[derive(Clone, PartialEq, Eq, Debug)]
86+
pub enum CollectionAllocErr {
87+
/// Error due to the computed capacity exceeding the collection's maximum
88+
/// (usually `isize::MAX` bytes).
89+
CapacityOverflow,
90+
/// Error due to the allocator (see the `AllocErr` type's docs).
91+
AllocErr,
92+
}

src/map.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use core::marker::PhantomData;
1818
use core::mem;
1919
use core::ops::Index;
2020
use raw::{Bucket, RawDrain, RawIntoIter, RawIter, RawTable};
21+
use CollectionAllocErr;
2122

2223
pub use fx::FxHashBuilder as DefaultHashBuilder;
2324

@@ -361,6 +362,29 @@ where
361362
.reserve(additional, |x| make_hash(hash_builder, &x.0));
362363
}
363364

365+
/// Tries to reserve capacity for at least `additional` more elements to be inserted
366+
/// in the given `HashMap<K,V>`. The collection may reserve more space to avoid
367+
/// frequent reallocations.
368+
///
369+
/// # Errors
370+
///
371+
/// If the capacity overflows, or the allocator reports a failure, then an error
372+
/// is returned.
373+
///
374+
/// # Examples
375+
///
376+
/// ```
377+
/// use hashbrown::HashMap;
378+
/// let mut map: HashMap<&str, isize> = HashMap::new();
379+
/// map.try_reserve(10).expect("why is the test harness OOMing on 10 bytes?");
380+
/// ```
381+
#[inline]
382+
pub fn try_reserve(&mut self, additional: usize) -> Result<(), CollectionAllocErr> {
383+
let hash_builder = &self.hash_builder;
384+
self.table
385+
.try_reserve(additional, |x| make_hash(hash_builder, &x.0))
386+
}
387+
364388
/// Shrinks the capacity of the map as much as possible. It will drop
365389
/// down as much as possible while maintaining the internal rules
366390
/// and possibly leaving some space in accordance with the resize policy.
@@ -2519,10 +2543,10 @@ mod test_map {
25192543
use super::HashMap;
25202544
use rand::{thread_rng, Rng};
25212545
use std::cell::RefCell;
2522-
#[cfg(try_reserve_not_implemented)]
25232546
use std::mem::size_of;
25242547
use std::usize;
25252548
use std::vec::Vec;
2549+
use CollectionAllocErr::*;
25262550

25272551
#[test]
25282552
fn test_zero_capacities() {
@@ -3397,21 +3421,20 @@ mod test_map {
33973421
panic!("Adaptive early resize failed");
33983422
}
33993423

3400-
#[cfg(try_reserve_not_implemented)]
34013424
#[test]
34023425
fn test_try_reserve() {
34033426
let mut empty_bytes: HashMap<u8, u8> = HashMap::new();
34043427

34053428
const MAX_USIZE: usize = usize::MAX;
34063429

34073430
// HashMap and RawTables use complicated size calculations
3408-
// hashes_size is sizeof(HashUint) * capacity;
3431+
// hashes_size is sizeof(u8) * capacity;
34093432
// pairs_size is sizeof((K. V)) * capacity;
34103433
// alignment_hashes_size is 8
34113434
// alignment_pairs size is 4
3412-
let size_of_multiplier = (size_of::<usize>() + size_of::<(u8, u8)>()).next_power_of_two();
3435+
let size_of_multiplier = (size_of::<u8>() + size_of::<(u8, u8)>()).next_power_of_two();
34133436
// The following formula is used to calculate the new capacity
3414-
let max_no_ovf = ((MAX_USIZE / 11) * 10) / size_of_multiplier - 1;
3437+
let max_no_ovf = ((MAX_USIZE / 8) * 7) / size_of_multiplier - 1;
34153438

34163439
if let Err(CapacityOverflow) = empty_bytes.try_reserve(MAX_USIZE) {
34173440
} else {

src/raw/mod.rs

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use core::mem::ManuallyDrop;
88
use core::ops::Range;
99
use core::ptr::NonNull;
1010
use scopeguard::guard;
11+
use CollectionAllocErr;
1112

1213
// Branch prediction hint. This is currently only available on nightly but it
1314
// consistently improves performance by 10-15%.
@@ -61,6 +62,32 @@ mod bitmask;
6162
use self::bitmask::BitMask;
6263
use self::imp::Group;
6364

65+
/// Whether memory allocation errors should return an error or abort.
66+
enum Fallibility {
67+
Fallible,
68+
Infallible,
69+
}
70+
71+
impl Fallibility {
72+
/// Error to return on capacity overflow.
73+
#[inline]
74+
fn capacity_overflow(&self) -> CollectionAllocErr {
75+
match *self {
76+
Fallibility::Fallible => CollectionAllocErr::CapacityOverflow,
77+
Fallibility::Infallible => panic!("Hash table capacity overflow"),
78+
}
79+
}
80+
81+
/// Error to return on allocation error.
82+
#[inline]
83+
fn alloc_err(&self, layout: Layout) -> CollectionAllocErr {
84+
match *self {
85+
Fallibility::Fallible => CollectionAllocErr::AllocErr,
86+
Fallibility::Infallible => handle_alloc_error(layout),
87+
}
88+
}
89+
}
90+
6491
/// Control byte value for an empty bucket.
6592
const EMPTY: u8 = 0b11111111;
6693

@@ -129,18 +156,23 @@ impl Iterator for ProbeSeq {
129156

130157
/// Returns the number of buckets needed to hold the given number of items,
131158
/// taking the maximum load factor into account.
159+
///
160+
/// Returns `None` if an overflow occurs.
132161
#[inline]
133-
fn capacity_to_buckets(cap: usize) -> usize {
162+
fn capacity_to_buckets(cap: usize) -> Option<usize> {
134163
let adjusted_cap = if cap < 8 {
135164
// Need at least 1 free bucket on small tables
136165
cap + 1
137166
} else {
138167
// Otherwise require 1/8 buckets to be empty (87.5% load)
139-
cap.checked_mul(8).expect("Hash table capacity overflow") / 7
168+
//
169+
// Be careful when modifying this, calculate_layout relies on the
170+
// overflow check here.
171+
cap.checked_mul(8)? / 7
140172
};
141173

142174
// Any overflows will have been caught by the checked_mul.
143-
adjusted_cap.next_power_of_two()
175+
Some(adjusted_cap.next_power_of_two())
144176
}
145177

146178
/// Returns the maximum effective capacity for the given bucket mask, taking
@@ -156,6 +188,8 @@ fn bucket_mask_to_capacity(bucket_mask: usize) -> usize {
156188

157189
// Returns a Layout which describes the allocation required for a hash table,
158190
// and the offset of the buckets in the allocation.
191+
///
192+
/// Returns `None` if an overflow occurs.
159193
#[inline]
160194
#[cfg(feature = "nightly")]
161195
fn calculate_layout<T>(buckets: usize) -> Option<(Layout, usize)> {
@@ -169,10 +203,10 @@ fn calculate_layout<T>(buckets: usize) -> Option<(Layout, usize)> {
169203
// We add `Group::WIDTH` control bytes at the end of the array which
170204
// replicate the bytes at the start of the array and thus avoids the need to
171205
// perform bounds-checking while probing.
172-
let ctrl = Layout::array::<u8>(buckets + Group::WIDTH)
173-
.ok()?
174-
.align_to(Group::WIDTH)
175-
.ok()?;
206+
//
207+
// There is no possible overflow here since buckets is a power of two and
208+
// Group::WIDTH is a small number.
209+
let ctrl = unsafe { Layout::from_size_align_unchecked(buckets + Group::WIDTH, Group::WIDTH) };
176210

177211
ctrl.extend(data).ok()
178212
}
@@ -188,12 +222,11 @@ fn calculate_layout<T>(buckets: usize) -> Option<(Layout, usize)> {
188222
let data_align = usize::max(mem::align_of::<T>(), Group::WIDTH);
189223
let data_offset = (buckets + Group::WIDTH).checked_add(data_align - 1)? & !(data_align - 1);
190224
let len = data_offset.checked_add(mem::size_of::<T>().checked_mul(buckets)?)?;
191-
unsafe {
192-
Some((
193-
Layout::from_size_align_unchecked(len, data_align),
194-
data_offset,
195-
))
196-
}
225+
226+
Some((
227+
unsafe { Layout::from_size_align_unchecked(len, data_align) },
228+
data_offset,
229+
))
197230
}
198231

199232
/// A reference to a hash table bucket containing a `T`.
@@ -271,28 +304,36 @@ impl<T> RawTable<T> {
271304
///
272305
/// The control bytes are left uninitialized.
273306
#[inline]
274-
unsafe fn new_uninitialized(buckets: usize) -> RawTable<T> {
307+
unsafe fn new_uninitialized(
308+
buckets: usize,
309+
fallability: Fallibility,
310+
) -> Result<RawTable<T>, CollectionAllocErr> {
275311
let (layout, data_offset) =
276-
calculate_layout::<T>(buckets).expect("Hash table capacity overflow");
277-
let ctrl = NonNull::new(alloc(layout)).unwrap_or_else(|| handle_alloc_error(layout));
312+
calculate_layout::<T>(buckets).ok_or_else(|| fallability.capacity_overflow())?;
313+
let ctrl = NonNull::new(alloc(layout)).ok_or_else(|| fallability.alloc_err(layout))?;
278314
let data = NonNull::new_unchecked(ctrl.as_ptr().add(data_offset) as *mut T);
279-
RawTable {
315+
Ok(RawTable {
280316
data,
281317
ctrl,
282318
bucket_mask: buckets - 1,
283319
items: 0,
284320
growth_left: bucket_mask_to_capacity(buckets - 1),
285-
}
321+
})
286322
}
287323

288-
/// Allocates a new hash table with at least enough capacity for inserting
289-
/// the given number of elements without reallocating.
290-
pub fn with_capacity(capacity: usize) -> RawTable<T> {
324+
/// Attempts to allocate a new hash table with at least enough capacity
325+
/// for inserting the given number of elements without reallocating.
326+
fn try_with_capacity(
327+
capacity: usize,
328+
fallability: Fallibility,
329+
) -> Result<RawTable<T>, CollectionAllocErr> {
291330
if capacity == 0 {
292-
RawTable::new()
331+
Ok(RawTable::new())
293332
} else {
294333
unsafe {
295-
let result = RawTable::new_uninitialized(capacity_to_buckets(capacity));
334+
let buckets =
335+
capacity_to_buckets(capacity).ok_or_else(|| fallability.capacity_overflow())?;
336+
let result = RawTable::new_uninitialized(buckets, fallability)?;
296337
result
297338
.ctrl(0)
298339
.write_bytes(EMPTY, result.buckets() + Group::WIDTH);
@@ -306,11 +347,18 @@ impl<T> RawTable<T> {
306347
.write_bytes(DELETED, Group::WIDTH - result.buckets());
307348
}
308349

309-
result
350+
Ok(result)
310351
}
311352
}
312353
}
313354

355+
/// Allocates a new hash table with at least enough capacity for inserting
356+
/// the given number of elements without reallocating.
357+
pub fn with_capacity(capacity: usize) -> RawTable<T> {
358+
RawTable::try_with_capacity(capacity, Fallibility::Infallible)
359+
.unwrap_or_else(|_| unsafe { hint::unreachable_unchecked() })
360+
}
361+
314362
/// Deallocates the table without dropping any entries.
315363
#[inline]
316364
unsafe fn free_buckets(&mut self) {
@@ -473,8 +521,9 @@ impl<T> RawTable<T> {
473521
#[inline]
474522
pub fn shrink_to(&mut self, min_size: usize, hasher: impl Fn(&T) -> u64) {
475523
let min_size = usize::max(self.items, min_size);
476-
if bucket_mask_to_capacity(self.bucket_mask) >= min_size * 2 {
477-
self.resize(min_size, hasher);
524+
if self.bucket_mask != 0 && bucket_mask_to_capacity(self.bucket_mask) >= min_size * 2 {
525+
self.resize(min_size, hasher, Fallibility::Infallible)
526+
.unwrap_or_else(|_| unsafe { hint::unreachable_unchecked() });
478527
}
479528
}
480529

@@ -483,25 +532,47 @@ impl<T> RawTable<T> {
483532
#[inline]
484533
pub fn reserve(&mut self, additional: usize, hasher: impl Fn(&T) -> u64) {
485534
if additional > self.growth_left {
486-
self.reserve_rehash(additional, hasher);
535+
self.reserve_rehash(additional, hasher, Fallibility::Infallible)
536+
.unwrap_or_else(|_| unsafe { hint::unreachable_unchecked() });
537+
}
538+
}
539+
540+
/// Tries to ensure that at least `additional` items can be inserted into
541+
/// the table without reallocation.
542+
#[inline]
543+
pub fn try_reserve(
544+
&mut self,
545+
additional: usize,
546+
hasher: impl Fn(&T) -> u64,
547+
) -> Result<(), CollectionAllocErr> {
548+
if additional > self.growth_left {
549+
self.reserve_rehash(additional, hasher, Fallibility::Fallible)
550+
} else {
551+
Ok(())
487552
}
488553
}
489554

490-
/// Out-of-line slow path for `reserve`.
555+
/// Out-of-line slow path for `reserve` and `try_reserve`.
491556
#[cold]
492557
#[inline(never)]
493-
fn reserve_rehash(&mut self, additional: usize, hasher: impl Fn(&T) -> u64) {
558+
fn reserve_rehash(
559+
&mut self,
560+
additional: usize,
561+
hasher: impl Fn(&T) -> u64,
562+
fallability: Fallibility,
563+
) -> Result<(), CollectionAllocErr> {
494564
let new_items = self
495565
.items
496566
.checked_add(additional)
497-
.expect("Hash table capacity overflow");
567+
.ok_or_else(|| fallability.capacity_overflow())?;
498568

499569
// Rehash in-place without re-allocating if we have plenty of spare
500570
// capacity that is locked up due to DELETED entries.
501571
if new_items < bucket_mask_to_capacity(self.bucket_mask) / 2 {
502572
self.rehash_in_place(hasher);
573+
Ok(())
503574
} else {
504-
self.resize(new_items, hasher);
575+
self.resize(new_items, hasher, fallability)
505576
}
506577
}
507578

@@ -607,12 +678,17 @@ impl<T> RawTable<T> {
607678

608679
/// Allocates a new table of a different size and moves the contents of the
609680
/// current table into it.
610-
fn resize(&mut self, capacity: usize, hasher: impl Fn(&T) -> u64) {
681+
fn resize(
682+
&mut self,
683+
capacity: usize,
684+
hasher: impl Fn(&T) -> u64,
685+
fallability: Fallibility,
686+
) -> Result<(), CollectionAllocErr> {
611687
unsafe {
612688
debug_assert!(self.items <= capacity);
613689

614690
// Allocate and initialize the new table.
615-
let mut new_table = RawTable::with_capacity(capacity);
691+
let mut new_table = RawTable::try_with_capacity(capacity, fallability)?;
616692
new_table.growth_left -= self.items;
617693
new_table.items = self.items;
618694

@@ -642,6 +718,8 @@ impl<T> RawTable<T> {
642718
// the items will not be dropped (since they have been moved into the
643719
// new table).
644720
mem::swap(self, &mut new_table);
721+
722+
Ok(())
645723
}
646724
}
647725

@@ -762,7 +840,10 @@ impl<T: Clone> Clone for RawTable<T> {
762840
Self::new()
763841
} else {
764842
unsafe {
765-
let mut new_table = ManuallyDrop::new(Self::new_uninitialized(self.buckets()));
843+
let mut new_table = ManuallyDrop::new(
844+
Self::new_uninitialized(self.buckets(), Fallibility::Infallible)
845+
.unwrap_or_else(|_| hint::unreachable_unchecked()),
846+
);
766847

767848
// Copy the control bytes unchanged. We do this in a single pass
768849
self.ctrl(0)

0 commit comments

Comments
 (0)