Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ serde_test = "1.0"
doc-comment = "0.3.1"
bumpalo = { version = "3.13.0", features = ["allocator-api2"] }

[target.'cfg(unix)'.dev-dependencies]
libc = "0.2.155"

[features]
default = ["default-hasher", "inline-more", "allocator-api2", "equivalent", "raw-entry"]

Expand Down
38 changes: 38 additions & 0 deletions benches/with_capacity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#![feature(test)]

extern crate test;

use hashbrown::HashMap;
use test::{black_box, Bencher};

type Map<K, V> = HashMap<K, V>;

macro_rules! bench_with_capacity {
($name:ident, $cap:expr) => {
#[bench]
fn $name(b: &mut Bencher) {
b.iter(|| {
// Construct a new empty map with a given capacity and return it to avoid
// being optimized away. Dropping it measures allocation + minimal setup.
let m: Map<usize, usize> = Map::with_capacity($cap);
black_box(m)
});
}
};
}

bench_with_capacity!(with_capacity_000000, 0);
bench_with_capacity!(with_capacity_000001, 1);
bench_with_capacity!(with_capacity_000003, 3);
bench_with_capacity!(with_capacity_000007, 7);
bench_with_capacity!(with_capacity_000008, 8);
bench_with_capacity!(with_capacity_000016, 16);
bench_with_capacity!(with_capacity_000032, 32);
bench_with_capacity!(with_capacity_000064, 64);
bench_with_capacity!(with_capacity_000128, 128);
bench_with_capacity!(with_capacity_000256, 256);
bench_with_capacity!(with_capacity_000512, 512);
bench_with_capacity!(with_capacity_001024, 1024);
bench_with_capacity!(with_capacity_004096, 4096);
bench_with_capacity!(with_capacity_016384, 16384);
bench_with_capacity!(with_capacity_065536, 65536);
133 changes: 133 additions & 0 deletions src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6631,3 +6631,136 @@ mod test_map {
);
}
}

#[cfg(all(test, unix, any(feature = "nightly", feature = "allocator-api2")))]
mod test_map_with_mmap_allocations {
use super::HashMap;
use crate::raw::prev_pow2;
use core::alloc::Layout;
use core::ptr::{null_mut, NonNull};

#[cfg(feature = "nightly")]
use core::alloc::{AllocError, Allocator};

#[cfg(all(feature = "allocator-api2", not(feature = "nightly")))]
use allocator_api2::alloc::{AllocError, Allocator};

/// This is not a production quality allocator, just good enough for
/// some basic tests.
#[derive(Clone, Copy, Debug)]
struct MmapAllocator {
/// Guarantee this is a power of 2.
page_size: usize,
}

impl MmapAllocator {
fn new() -> Result<Self, AllocError> {
let result = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if result < 1 {
return Err(AllocError);
}

let page_size = result as usize;
if !page_size.is_power_of_two() {
Err(AllocError)
} else {
Ok(Self { page_size })
}
}

fn fit_to_page_size(&self, n: usize) -> Result<usize, AllocError> {
// If n=0, give a single page (wasteful, I know).
let n = if n == 0 { self.page_size } else { n };

match n & (self.page_size - 1) {
0 => Ok(n),
rem => n.checked_add(self.page_size - rem).ok_or(AllocError),
}
}
}

unsafe impl Allocator for MmapAllocator {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
if layout.align() > self.page_size {
return Err(AllocError);
}

let null = null_mut();
let len = self.fit_to_page_size(layout.size())? as libc::size_t;
let prot = libc::PROT_READ | libc::PROT_WRITE;
let flags = libc::MAP_PRIVATE | libc::MAP_ANON;
let addr = unsafe { libc::mmap(null, len, prot, flags, -1, 0) };

// mmap returns MAP_FAILED on failure, not Null.
if addr == libc::MAP_FAILED {
return Err(AllocError);
}

match NonNull::new(addr.cast()) {
Some(data) => {
// SAFETY: this is NonNull::slice_from_raw_parts.
Ok(unsafe {
NonNull::new_unchecked(core::ptr::slice_from_raw_parts_mut(
data.as_ptr(),
len,
))
})
}

// This branch shouldn't be taken in practice, but since we
// cannot return null as a valid pointer in our type system,
// we attempt to handle it.
None => {
_ = unsafe { libc::munmap(addr, len) };
Err(AllocError)
}
}
}

unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
// If they allocated it with this layout, it must round correctly.
let size = self.fit_to_page_size(layout.size()).unwrap();
let _result = libc::munmap(ptr.as_ptr().cast(), size);
debug_assert_eq!(0, _result)
}
}

#[test]
fn test_tiny_allocation_gets_rounded_to_page_size() {
let alloc = MmapAllocator::new().unwrap();
let mut map: HashMap<usize, (), _, _> = HashMap::with_capacity_in(1, alloc);

// Size of an element plus its control byte.
let rough_bucket_size = core::mem::size_of::<(usize, ())>() + 1;

// Accounting for some misc. padding that's likely in the allocation
// due to rounding to group width, etc.
let overhead = 3 * core::mem::size_of::<usize>();
let num_buckets = (alloc.page_size - overhead) / rough_bucket_size;
// Buckets are always powers of 2.
let min_elems = prev_pow2(num_buckets);
// Real load-factor is 7/8, but this is a lower estimation, so 1/2.
let min_capacity = min_elems >> 1;
let capacity = map.capacity();
assert!(
capacity >= min_capacity,
"failed: {capacity} >= {min_capacity}"
);

// Fill it up.
for i in 0..capacity {
map.insert(i, ());
}
// Capacity should not have changed and it should be full.
assert_eq!(capacity, map.len());
assert_eq!(capacity, map.capacity());

// Alright, make it grow.
map.insert(capacity, ());
assert!(
capacity < map.capacity(),
"failed: {capacity} < {}",
map.capacity()
);
}
}
27 changes: 19 additions & 8 deletions src/raw/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ mod inner {
use core::ptr::NonNull;

#[allow(clippy::map_err_ignore)]
pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<u8>, ()> {
pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<[u8]>, ()> {
match alloc.allocate(layout) {
Ok(ptr) => Ok(ptr.as_non_null_ptr()),
Ok(ptr) => Ok(ptr),
Err(_) => Err(()),
}
}
Expand All @@ -38,9 +38,9 @@ mod inner {
use core::ptr::NonNull;

#[allow(clippy::map_err_ignore)]
pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<u8>, ()> {
pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<[u8]>, ()> {
match alloc.allocate(layout) {
Ok(ptr) => Ok(ptr.cast()),
Ok(ptr) => Ok(ptr),
Err(_) => Err(()),
}
}
Expand All @@ -61,7 +61,7 @@ mod inner {

#[allow(clippy::missing_safety_doc)] // not exposed outside of this crate
pub unsafe trait Allocator {
fn allocate(&self, layout: Layout) -> Result<NonNull<u8>, ()>;
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, ()>;
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout);
}

Expand All @@ -70,8 +70,19 @@ mod inner {

unsafe impl Allocator for Global {
#[inline]
fn allocate(&self, layout: Layout) -> Result<NonNull<u8>, ()> {
unsafe { NonNull::new(alloc(layout)).ok_or(()) }
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, ()> {
match unsafe { NonNull::new(alloc(layout)) } {
Some(data) => {
// SAFETY: this is NonNull::slice_from_raw_parts.
Ok(unsafe {
NonNull::new_unchecked(core::ptr::slice_from_raw_parts_mut(
data.as_ptr(),
layout.size(),
))
})
}
None => Err(()),
}
}
#[inline]
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
Expand All @@ -86,7 +97,7 @@ mod inner {
}
}

pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<u8>, ()> {
pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<[u8]>, ()> {
alloc.allocate(layout)
}
}
75 changes: 72 additions & 3 deletions src/raw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,40 @@ impl RawTableInner {
}
}

/// Find the previous power of 2. If it's already a power of 2, it's unchanged.
/// Passing zero is undefined behavior.
pub(crate) fn prev_pow2(z: usize) -> usize {
let shift = mem::size_of::<usize>() * 8 - 1;
1 << (shift - (z.leading_zeros() as usize))
}

fn maximum_buckets_in(
allocation_size: usize,
table_layout: TableLayout,
group_width: usize,
) -> usize {
// Given an equation like:
// z >= x * y + x + g
// x can be maximized by doing:
// x = (z - g) / (y + 1)
// If you squint:
// x is the number of buckets
// y is the table_layout.size
// z is the size of the allocation
// g is the group width
// But this is ignoring the padding needed for ctrl_align.
// If we remember these restrictions:
// x is always a power of 2
// Layout size for T must always be a multiple of T
// Then the alignment can be ignored if we add the constraint:
// x * y >= table_layout.ctrl_align
Copy link
Member

Choose a reason for hiding this comment

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

I had to spend some time to work out why this was correct. The key insight here is that ctrl_offset = align(x * y, ctrl_align) in the layout calculation. It might be worth adding a line to the comment with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left a comment, take a look and suggest more if you can think of something more to note.

// This is taken care of by `capacity_to_buckets`.
Copy link
Member

Choose a reason for hiding this comment

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

This is brittle: currently capacity_to_buckets doesn't actually guarantee this. If we are going to rely on this guarantee then capacity_to_buckets needs a debug_assert and a comment indicating that maximum_buckets_in relies on this behavior. This will remind anyone who changes capacity_to_buckets in the future to also adjust maximum_buckets_in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since #615, it does guarantee this. Well, assuming I didn't make a mistake anyway ^_^. I tried finding issues with it but I don't see any, let me know if you see something.

I think adding assertions can be helpful to make this more obvious and be more resilient to future changes, so I've added ensure_bucket_bytes_at_least_ctrl_align. Take a look.

let numerator = allocation_size - group_width;
let denominator = table_layout.size + 1; // todo: ZSTs?
let quotient = numerator / denominator;
prev_pow2(quotient)
}

impl RawTableInner {
/// Allocates a new [`RawTableInner`] with the given number of buckets.
/// The control bytes and buckets are left uninitialized.
Expand All @@ -1459,7 +1493,7 @@ impl RawTableInner {
unsafe fn new_uninitialized<A>(
alloc: &A,
table_layout: TableLayout,
buckets: usize,
mut buckets: usize,
fallibility: Fallibility,
) -> Result<Self, TryReserveError>
where
Expand All @@ -1468,13 +1502,31 @@ impl RawTableInner {
debug_assert!(buckets.is_power_of_two());

// Avoid `Option::ok_or_else` because it bloats LLVM IR.
let (layout, ctrl_offset) = match table_layout.calculate_layout_for(buckets) {
let (layout, mut ctrl_offset) = match table_layout.calculate_layout_for(buckets) {
Some(lco) => lco,
None => return Err(fallibility.capacity_overflow()),
};

let ptr: NonNull<u8> = match do_alloc(alloc, layout) {
Ok(block) => block.cast(),
Ok(block) => {
if block.len() > layout.size() {
// Utilize over-sized allocations.
let x = maximum_buckets_in(block.len(), table_layout, Group::WIDTH);
debug_assert!(x >= buckets);
// Calculate the new ctrl_offset.
let (_oversized_layout, oversized_ctrl_offset) =
match table_layout.calculate_layout_for(x) {
Some(lco) => lco,
None => unsafe { hint::unreachable_unchecked() },
};
debug_assert!(_oversized_layout.size() <= block.len());
debug_assert!(oversized_ctrl_offset >= ctrl_offset);
ctrl_offset = oversized_ctrl_offset;
buckets = x;
}

block.cast()
}
Err(_) => return Err(fallibility.alloc_err(layout)),
};

Expand Down Expand Up @@ -4168,6 +4220,23 @@ impl<T, A: Allocator> RawExtractIf<'_, T, A> {
mod test_map {
use super::*;

#[test]
fn test_prev_pow2() {
// Skip 0, not defined for that input.
let mut pow2: usize = 1;
while (pow2 << 1) > 0 {
let next_pow2 = pow2 << 1;
assert_eq!(pow2, prev_pow2(pow2));
// Need to skip 2, because it's also a power of 2, so it doesn't
// return the previous power of 2.
if next_pow2 > 2 {
assert_eq!(pow2, prev_pow2(pow2 + 1));
assert_eq!(pow2, prev_pow2(next_pow2 - 1));
}
pow2 = next_pow2;
}
}

#[test]
fn test_minimum_capacity_for_small_types() {
#[track_caller]
Expand Down
Loading