Skip to content

Commit b8c0e54

Browse files
committed
Make rehashing and resizing less generic
1 parent d5002fa commit b8c0e54

File tree

1 file changed

+209
-110
lines changed

1 file changed

+209
-110
lines changed

src/raw/mod.rs

Lines changed: 209 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ use crate::scopeguard::guard;
33
use crate::TryReserveError;
44
#[cfg(feature = "nightly")]
55
use crate::UnavailableMutError;
6-
use core::hint;
76
use core::iter::FusedIterator;
87
use core::marker::PhantomData;
98
use core::mem;
109
use core::mem::ManuallyDrop;
1110
#[cfg(feature = "nightly")]
1211
use core::mem::MaybeUninit;
1312
use core::ptr::NonNull;
13+
use core::{hint, ptr};
1414

1515
cfg_if! {
1616
// Use the SSE2 implementation if possible: it allows us to scan 16 buckets
@@ -359,6 +359,7 @@ impl<T> Bucket<T> {
359359
pub unsafe fn as_mut<'a>(&self) -> &'a mut T {
360360
&mut *self.as_ptr()
361361
}
362+
#[cfg(feature = "raw")]
362363
#[cfg_attr(feature = "inline-more", inline)]
363364
pub unsafe fn copy_from_nonoverlapping(&self, other: &Self) {
364365
self.as_ptr().copy_from_nonoverlapping(other.as_ptr(), 1);
@@ -682,24 +683,14 @@ impl<T, A: Allocator + Clone> RawTable<T, A> {
682683
hasher: impl Fn(&T) -> u64,
683684
fallibility: Fallibility,
684685
) -> Result<(), TryReserveError> {
685-
// Avoid `Option::ok_or_else` because it bloats LLVM IR.
686-
let new_items = match self.table.items.checked_add(additional) {
687-
Some(new_items) => new_items,
688-
None => return Err(fallibility.capacity_overflow()),
689-
};
690-
let full_capacity = bucket_mask_to_capacity(self.table.bucket_mask);
691-
if new_items <= full_capacity / 2 {
692-
// Rehash in-place without re-allocating if we have plenty of spare
693-
// capacity that is locked up due to DELETED entries.
694-
self.rehash_in_place(hasher);
695-
Ok(())
696-
} else {
697-
// Otherwise, conservatively resize to at least the next size up
698-
// to avoid churning deletes into frequent rehashes.
699-
self.resize(
700-
usize::max(new_items, full_capacity + 1),
701-
hasher,
686+
unsafe {
687+
self.table.reserve_rehash_inner(
688+
additional,
689+
&|table, index| hasher(table.bucket::<T>(index).as_ref()),
702690
fallibility,
691+
TableLayout::new::<T>(),
692+
mem::transmute(ptr::drop_in_place::<T> as unsafe fn(*mut T)),
693+
mem::needs_drop::<T>(),
703694
)
704695
}
705696
}
@@ -708,76 +699,15 @@ impl<T, A: Allocator + Clone> RawTable<T, A> {
708699
/// allocation).
709700
///
710701
/// If `hasher` panics then some the table's contents may be lost.
702+
#[cfg(test)]
711703
fn rehash_in_place(&mut self, hasher: impl Fn(&T) -> u64) {
712704
unsafe {
713-
// If the hash function panics then properly clean up any elements
714-
// that we haven't rehashed yet. We unfortunately can't preserve the
715-
// element since we lost their hash and have no way of recovering it
716-
// without risking another panic.
717-
self.table.prepare_rehash_in_place();
718-
719-
let mut guard = guard(&mut self.table, move |self_| {
720-
if mem::needs_drop::<T>() {
721-
for i in 0..self_.buckets() {
722-
if *self_.ctrl(i) == DELETED {
723-
self_.set_ctrl(i, EMPTY);
724-
self_.bucket::<T>(i).drop();
725-
self_.items -= 1;
726-
}
727-
}
728-
}
729-
self_.growth_left = bucket_mask_to_capacity(self_.bucket_mask) - self_.items;
730-
});
731-
732-
// At this point, DELETED elements are elements that we haven't
733-
// rehashed yet. Find them and re-insert them at their ideal
734-
// position.
735-
'outer: for i in 0..guard.buckets() {
736-
if *guard.ctrl(i) != DELETED {
737-
continue;
738-
}
739-
740-
'inner: loop {
741-
// Hash the current item
742-
let item = guard.bucket(i);
743-
let hash = hasher(item.as_ref());
744-
745-
// Search for a suitable place to put it
746-
let new_i = guard.find_insert_slot(hash);
747-
748-
// Probing works by scanning through all of the control
749-
// bytes in groups, which may not be aligned to the group
750-
// size. If both the new and old position fall within the
751-
// same unaligned group, then there is no benefit in moving
752-
// it and we can just continue to the next item.
753-
if likely(guard.is_in_same_group(i, new_i, hash)) {
754-
guard.set_ctrl_h2(i, hash);
755-
continue 'outer;
756-
}
757-
758-
// We are moving the current item to a new position. Write
759-
// our H2 to the control byte of the new position.
760-
let prev_ctrl = guard.replace_ctrl_h2(new_i, hash);
761-
if prev_ctrl == EMPTY {
762-
guard.set_ctrl(i, EMPTY);
763-
// If the target slot is empty, simply move the current
764-
// element into the new slot and clear the old control
765-
// byte.
766-
guard.bucket(new_i).copy_from_nonoverlapping(&item);
767-
continue 'outer;
768-
} else {
769-
// If the target slot is occupied, swap the two elements
770-
// and then continue processing the element that we just
771-
// swapped into the old slot.
772-
debug_assert_eq!(prev_ctrl, DELETED);
773-
mem::swap(guard.bucket(new_i).as_mut(), item.as_mut());
774-
continue 'inner;
775-
}
776-
}
777-
}
778-
779-
guard.growth_left = bucket_mask_to_capacity(guard.bucket_mask) - guard.items;
780-
mem::forget(guard);
705+
self.table.rehash_in_place(
706+
&|table, index| hasher(table.bucket::<T>(index).as_ref()),
707+
mem::size_of::<T>(),
708+
mem::transmute(ptr::drop_in_place::<T> as unsafe fn(*mut T)),
709+
mem::needs_drop::<T>(),
710+
);
781711
}
782712
}
783713

@@ -790,30 +720,12 @@ impl<T, A: Allocator + Clone> RawTable<T, A> {
790720
fallibility: Fallibility,
791721
) -> Result<(), TryReserveError> {
792722
unsafe {
793-
let mut new_table =
794-
self.table
795-
.prepare_resize(TableLayout::new::<T>(), capacity, fallibility)?;
796-
797-
// Copy all elements to the new table.
798-
for item in self.iter() {
799-
// This may panic.
800-
let hash = hasher(item.as_ref());
801-
802-
// We can use a simpler version of insert() here since:
803-
// - there are no DELETED entries.
804-
// - we know there is enough space in the table.
805-
// - all elements are unique.
806-
let (index, _) = new_table.prepare_insert_slot(hash);
807-
new_table.bucket(index).copy_from_nonoverlapping(&item);
808-
}
809-
810-
// We successfully copied all elements without panicking. Now replace
811-
// self with the new table. The old table will have its memory freed but
812-
// the items will not be dropped (since they have been moved into the
813-
// new table).
814-
mem::swap(&mut self.table, &mut new_table);
815-
816-
Ok(())
723+
self.table.resize_inner(
724+
capacity,
725+
&|table, index| hasher(table.bucket::<T>(index).as_ref()),
726+
fallibility,
727+
TableLayout::new::<T>(),
728+
)
817729
}
818730
}
819731

@@ -1312,6 +1224,19 @@ impl<A: Allocator + Clone> RawTableInner<A> {
13121224
Bucket::from_base_index(self.data_end(), index)
13131225
}
13141226

1227+
#[cfg_attr(feature = "inline-more", inline)]
1228+
unsafe fn bucket_ptr(&self, index: usize, size_of: usize) -> *mut u8 {
1229+
debug_assert_ne!(self.bucket_mask, 0);
1230+
debug_assert!(index < self.buckets());
1231+
let base: *mut u8 = self.data_end().as_ptr();
1232+
if size_of == 0 {
1233+
// FIXME: Check if this `data_end` is aligned with ZST?
1234+
base
1235+
} else {
1236+
base.sub((index + 1) * size_of)
1237+
}
1238+
}
1239+
13151240
#[cfg_attr(feature = "inline-more", inline)]
13161241
unsafe fn data_end<T>(&self) -> NonNull<T> {
13171242
NonNull::new_unchecked(self.ctrl.as_ptr().cast())
@@ -1457,6 +1382,180 @@ impl<A: Allocator + Clone> RawTableInner<A> {
14571382
}))
14581383
}
14591384

1385+
/// Reserves or rehashes to make room for `additional` more elements.
1386+
///
1387+
/// This uses dynamic dispatch to reduce the amount of
1388+
/// code generated, but it is eliminated by LLVM optimizations when inlined.
1389+
#[allow(clippy::inline_always)]
1390+
#[inline(always)]
1391+
unsafe fn reserve_rehash_inner(
1392+
&mut self,
1393+
additional: usize,
1394+
hasher: &dyn Fn(&mut Self, usize) -> u64,
1395+
fallibility: Fallibility,
1396+
layout: TableLayout,
1397+
drop: fn(*mut u8),
1398+
drops: bool,
1399+
) -> Result<(), TryReserveError> {
1400+
// Avoid `Option::ok_or_else` because it bloats LLVM IR.
1401+
let new_items = match self.items.checked_add(additional) {
1402+
Some(new_items) => new_items,
1403+
None => return Err(fallibility.capacity_overflow()),
1404+
};
1405+
let full_capacity = bucket_mask_to_capacity(self.bucket_mask);
1406+
if new_items <= full_capacity / 2 {
1407+
// Rehash in-place without re-allocating if we have plenty of spare
1408+
// capacity that is locked up due to DELETED entries.
1409+
self.rehash_in_place(hasher, layout.size, drop, drops);
1410+
Ok(())
1411+
} else {
1412+
// Otherwise, conservatively resize to at least the next size up
1413+
// to avoid churning deletes into frequent rehashes.
1414+
self.resize_inner(
1415+
usize::max(new_items, full_capacity + 1),
1416+
hasher,
1417+
fallibility,
1418+
layout,
1419+
)
1420+
}
1421+
}
1422+
1423+
/// Allocates a new table of a different size and moves the contents of the
1424+
/// current table into it.
1425+
///
1426+
/// This uses dynamic dispatch to reduce the amount of
1427+
/// code generated, but it is eliminated by LLVM optimizations when inlined.
1428+
#[allow(clippy::inline_always)]
1429+
#[inline(always)]
1430+
unsafe fn resize_inner(
1431+
&mut self,
1432+
capacity: usize,
1433+
hasher: &dyn Fn(&mut Self, usize) -> u64,
1434+
fallibility: Fallibility,
1435+
layout: TableLayout,
1436+
) -> Result<(), TryReserveError> {
1437+
let mut new_table = self.prepare_resize(layout, capacity, fallibility)?;
1438+
1439+
// Copy all elements to the new table.
1440+
for i in 0..self.buckets() {
1441+
if !is_full(*self.ctrl(i)) {
1442+
continue;
1443+
}
1444+
1445+
// This may panic.
1446+
let hash = hasher(self, i);
1447+
1448+
// We can use a simpler version of insert() here since:
1449+
// - there are no DELETED entries.
1450+
// - we know there is enough space in the table.
1451+
// - all elements are unique.
1452+
let (index, _) = new_table.prepare_insert_slot(hash);
1453+
1454+
ptr::copy_nonoverlapping(
1455+
self.bucket_ptr(i, layout.size),
1456+
new_table.bucket_ptr(index, layout.size),
1457+
layout.size,
1458+
);
1459+
}
1460+
1461+
// We successfully copied all elements without panicking. Now replace
1462+
// self with the new table. The old table will have its memory freed but
1463+
// the items will not be dropped (since they have been moved into the
1464+
// new table).
1465+
mem::swap(self, &mut new_table);
1466+
1467+
Ok(())
1468+
}
1469+
1470+
/// Rehashes the contents of the table in place (i.e. without changing the
1471+
/// allocation).
1472+
///
1473+
/// If `hasher` panics then some the table's contents may be lost.
1474+
///
1475+
/// This uses dynamic dispatch to reduce the amount of
1476+
/// code generated, but it is eliminated by LLVM optimizations when inlined.
1477+
#[allow(clippy::inline_always)]
1478+
#[inline(always)]
1479+
unsafe fn rehash_in_place(
1480+
&mut self,
1481+
hasher: &dyn Fn(&mut Self, usize) -> u64,
1482+
size_of: usize,
1483+
drop: fn(*mut u8),
1484+
drops: bool,
1485+
) {
1486+
// If the hash function panics then properly clean up any elements
1487+
// that we haven't rehashed yet. We unfortunately can't preserve the
1488+
// element since we lost their hash and have no way of recovering it
1489+
// without risking another panic.
1490+
self.prepare_rehash_in_place();
1491+
1492+
let mut guard = guard(self, move |self_| {
1493+
if drops {
1494+
for i in 0..self_.buckets() {
1495+
if *self_.ctrl(i) == DELETED {
1496+
self_.set_ctrl(i, EMPTY);
1497+
drop(self_.bucket_ptr(i, size_of));
1498+
self_.items -= 1;
1499+
}
1500+
}
1501+
}
1502+
self_.growth_left = bucket_mask_to_capacity(self_.bucket_mask) - self_.items;
1503+
});
1504+
1505+
// At this point, DELETED elements are elements that we haven't
1506+
// rehashed yet. Find them and re-insert them at their ideal
1507+
// position.
1508+
'outer: for i in 0..guard.buckets() {
1509+
if *guard.ctrl(i) != DELETED {
1510+
continue;
1511+
}
1512+
1513+
let i_p = guard.bucket_ptr(i, size_of);
1514+
1515+
'inner: loop {
1516+
// Hash the current item
1517+
let hash = hasher(*guard, i);
1518+
1519+
// Search for a suitable place to put it
1520+
let new_i = guard.find_insert_slot(hash);
1521+
let new_i_p = guard.bucket_ptr(new_i, size_of);
1522+
1523+
// Probing works by scanning through all of the control
1524+
// bytes in groups, which may not be aligned to the group
1525+
// size. If both the new and old position fall within the
1526+
// same unaligned group, then there is no benefit in moving
1527+
// it and we can just continue to the next item.
1528+
if likely(guard.is_in_same_group(i, new_i, hash)) {
1529+
guard.set_ctrl_h2(i, hash);
1530+
continue 'outer;
1531+
}
1532+
1533+
// We are moving the current item to a new position. Write
1534+
// our H2 to the control byte of the new position.
1535+
let prev_ctrl = guard.replace_ctrl_h2(new_i, hash);
1536+
if prev_ctrl == EMPTY {
1537+
guard.set_ctrl(i, EMPTY);
1538+
// If the target slot is empty, simply move the current
1539+
// element into the new slot and clear the old control
1540+
// byte.
1541+
ptr::copy_nonoverlapping(i_p, new_i_p, size_of);
1542+
continue 'outer;
1543+
} else {
1544+
// If the target slot is occupied, swap the two elements
1545+
// and then continue processing the element that we just
1546+
// swapped into the old slot.
1547+
debug_assert_eq!(prev_ctrl, DELETED);
1548+
ptr::swap_nonoverlapping(i_p, new_i_p, size_of);
1549+
continue 'inner;
1550+
}
1551+
}
1552+
}
1553+
1554+
guard.growth_left = bucket_mask_to_capacity(guard.bucket_mask) - guard.items;
1555+
1556+
mem::forget(guard);
1557+
}
1558+
14601559
#[inline]
14611560
unsafe fn free_buckets(&mut self, table_layout: TableLayout) {
14621561
// Avoid `Option::unwrap_or_else` because it bloats LLVM IR.

0 commit comments

Comments
 (0)