Skip to content

Commit 64aa7d5

Browse files
authored
Merge pull request #523 from morrisonlevi/oversized-allocations
feat: recognize and use over sized allocations
2 parents 957b590 + 88919fc commit 64aa7d5

File tree

5 files changed

+294
-14
lines changed

5 files changed

+294
-14
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ serde_test = "1.0"
4747
doc-comment = "0.3.1"
4848
bumpalo = { version = "3.13.0", features = ["allocator-api2"] }
4949

50+
[target.'cfg(unix)'.dev-dependencies]
51+
libc = "0.2.155"
52+
5053
[features]
5154
default = ["default-hasher", "inline-more", "allocator-api2", "equivalent", "raw-entry"]
5255

benches/with_capacity.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#![feature(test)]
2+
3+
extern crate test;
4+
5+
use hashbrown::HashMap;
6+
use test::{black_box, Bencher};
7+
8+
type Map<K, V> = HashMap<K, V>;
9+
10+
macro_rules! bench_with_capacity {
11+
($name:ident, $cap:expr) => {
12+
#[bench]
13+
fn $name(b: &mut Bencher) {
14+
b.iter(|| {
15+
// Construct a new empty map with a given capacity and return it to avoid
16+
// being optimized away. Dropping it measures allocation + minimal setup.
17+
let m: Map<usize, usize> = Map::with_capacity($cap);
18+
black_box(m)
19+
});
20+
}
21+
};
22+
}
23+
24+
bench_with_capacity!(with_capacity_000000, 0);
25+
bench_with_capacity!(with_capacity_000001, 1);
26+
bench_with_capacity!(with_capacity_000003, 3);
27+
bench_with_capacity!(with_capacity_000007, 7);
28+
bench_with_capacity!(with_capacity_000008, 8);
29+
bench_with_capacity!(with_capacity_000016, 16);
30+
bench_with_capacity!(with_capacity_000032, 32);
31+
bench_with_capacity!(with_capacity_000064, 64);
32+
bench_with_capacity!(with_capacity_000128, 128);
33+
bench_with_capacity!(with_capacity_000256, 256);
34+
bench_with_capacity!(with_capacity_000512, 512);
35+
bench_with_capacity!(with_capacity_001024, 1024);
36+
bench_with_capacity!(with_capacity_004096, 4096);
37+
bench_with_capacity!(with_capacity_016384, 16384);
38+
bench_with_capacity!(with_capacity_065536, 65536);

src/map.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6631,3 +6631,136 @@ mod test_map {
66316631
);
66326632
}
66336633
}
6634+
6635+
#[cfg(all(test, unix, any(feature = "nightly", feature = "allocator-api2")))]
6636+
mod test_map_with_mmap_allocations {
6637+
use super::HashMap;
6638+
use crate::raw::prev_pow2;
6639+
use core::alloc::Layout;
6640+
use core::ptr::{null_mut, NonNull};
6641+
6642+
#[cfg(feature = "nightly")]
6643+
use core::alloc::{AllocError, Allocator};
6644+
6645+
#[cfg(all(feature = "allocator-api2", not(feature = "nightly")))]
6646+
use allocator_api2::alloc::{AllocError, Allocator};
6647+
6648+
/// This is not a production quality allocator, just good enough for
6649+
/// some basic tests.
6650+
#[derive(Clone, Copy, Debug)]
6651+
struct MmapAllocator {
6652+
/// Guarantee this is a power of 2.
6653+
page_size: usize,
6654+
}
6655+
6656+
impl MmapAllocator {
6657+
fn new() -> Result<Self, AllocError> {
6658+
let result = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
6659+
if result < 1 {
6660+
return Err(AllocError);
6661+
}
6662+
6663+
let page_size = result as usize;
6664+
if !page_size.is_power_of_two() {
6665+
Err(AllocError)
6666+
} else {
6667+
Ok(Self { page_size })
6668+
}
6669+
}
6670+
6671+
fn fit_to_page_size(&self, n: usize) -> Result<usize, AllocError> {
6672+
// If n=0, give a single page (wasteful, I know).
6673+
let n = if n == 0 { self.page_size } else { n };
6674+
6675+
match n & (self.page_size - 1) {
6676+
0 => Ok(n),
6677+
rem => n.checked_add(self.page_size - rem).ok_or(AllocError),
6678+
}
6679+
}
6680+
}
6681+
6682+
unsafe impl Allocator for MmapAllocator {
6683+
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
6684+
if layout.align() > self.page_size {
6685+
return Err(AllocError);
6686+
}
6687+
6688+
let null = null_mut();
6689+
let len = self.fit_to_page_size(layout.size())? as libc::size_t;
6690+
let prot = libc::PROT_READ | libc::PROT_WRITE;
6691+
let flags = libc::MAP_PRIVATE | libc::MAP_ANON;
6692+
let addr = unsafe { libc::mmap(null, len, prot, flags, -1, 0) };
6693+
6694+
// mmap returns MAP_FAILED on failure, not Null.
6695+
if addr == libc::MAP_FAILED {
6696+
return Err(AllocError);
6697+
}
6698+
6699+
match NonNull::new(addr.cast()) {
6700+
Some(data) => {
6701+
// SAFETY: this is NonNull::slice_from_raw_parts.
6702+
Ok(unsafe {
6703+
NonNull::new_unchecked(core::ptr::slice_from_raw_parts_mut(
6704+
data.as_ptr(),
6705+
len,
6706+
))
6707+
})
6708+
}
6709+
6710+
// This branch shouldn't be taken in practice, but since we
6711+
// cannot return null as a valid pointer in our type system,
6712+
// we attempt to handle it.
6713+
None => {
6714+
_ = unsafe { libc::munmap(addr, len) };
6715+
Err(AllocError)
6716+
}
6717+
}
6718+
}
6719+
6720+
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
6721+
// If they allocated it with this layout, it must round correctly.
6722+
let size = self.fit_to_page_size(layout.size()).unwrap();
6723+
let _result = libc::munmap(ptr.as_ptr().cast(), size);
6724+
debug_assert_eq!(0, _result)
6725+
}
6726+
}
6727+
6728+
#[test]
6729+
fn test_tiny_allocation_gets_rounded_to_page_size() {
6730+
let alloc = MmapAllocator::new().unwrap();
6731+
let mut map: HashMap<usize, (), _, _> = HashMap::with_capacity_in(1, alloc);
6732+
6733+
// Size of an element plus its control byte.
6734+
let rough_bucket_size = core::mem::size_of::<(usize, ())>() + 1;
6735+
6736+
// Accounting for some misc. padding that's likely in the allocation
6737+
// due to rounding to group width, etc.
6738+
let overhead = 3 * core::mem::size_of::<usize>();
6739+
let num_buckets = (alloc.page_size - overhead) / rough_bucket_size;
6740+
// Buckets are always powers of 2.
6741+
let min_elems = prev_pow2(num_buckets);
6742+
// Real load-factor is 7/8, but this is a lower estimation, so 1/2.
6743+
let min_capacity = min_elems >> 1;
6744+
let capacity = map.capacity();
6745+
assert!(
6746+
capacity >= min_capacity,
6747+
"failed: {capacity} >= {min_capacity}"
6748+
);
6749+
6750+
// Fill it up.
6751+
for i in 0..capacity {
6752+
map.insert(i, ());
6753+
}
6754+
// Capacity should not have changed and it should be full.
6755+
assert_eq!(capacity, map.len());
6756+
assert_eq!(capacity, map.capacity());
6757+
6758+
// Alright, make it grow.
6759+
map.insert(capacity, ());
6760+
assert!(
6761+
capacity < map.capacity(),
6762+
"failed: {capacity} < {}",
6763+
map.capacity()
6764+
);
6765+
}
6766+
}

src/raw/alloc.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ mod inner {
1515
use core::ptr::NonNull;
1616

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

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

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

@@ -70,8 +70,19 @@ mod inner {
7070

7171
unsafe impl Allocator for Global {
7272
#[inline]
73-
fn allocate(&self, layout: Layout) -> Result<NonNull<u8>, ()> {
74-
unsafe { NonNull::new(alloc(layout)).ok_or(()) }
73+
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, ()> {
74+
match unsafe { NonNull::new(alloc(layout)) } {
75+
Some(data) => {
76+
// SAFETY: this is NonNull::slice_from_raw_parts.
77+
Ok(unsafe {
78+
NonNull::new_unchecked(core::ptr::slice_from_raw_parts_mut(
79+
data.as_ptr(),
80+
layout.size(),
81+
))
82+
})
83+
}
84+
None => Err(()),
85+
}
7586
}
7687
#[inline]
7788
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
@@ -86,7 +97,7 @@ mod inner {
8697
}
8798
}
8899

89-
pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<u8>, ()> {
100+
pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<[u8]>, ()> {
90101
alloc.allocate(layout)
91102
}
92103
}

0 commit comments

Comments
 (0)