Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: recognize and use over sized allocations #523

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -46,6 +46,9 @@ doc-comment = "0.3.1"
bumpalo = { version = "3.13.0", features = ["allocator-api2"] }
rkyv = { version = "0.7.42", features = ["validation"] }

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

[features]
default = ["ahash", "inline-more", "allocator-api2"]

Expand Down
110 changes: 110 additions & 0 deletions src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8958,3 +8958,113 @@ mod test_map {
assert_eq!(dropped.load(Ordering::SeqCst), 0);
}
}

#[cfg(all(test, unix))]
mod test_map_with_mmap_allocations {
use super::HashMap;
use crate::raw::prev_pow2;
use allocator_api2::alloc::{AllocError, Allocator};
use core::alloc::Layout;
use core::ptr::{null_mut, NonNull};

/// 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 size = self.fit_to_page_size(layout.size())?;
let null = null_mut();
let len = size as libc::size_t;
let prot = libc::PROT_READ | libc::PROT_WRITE;
let flags = libc::MAP_PRIVATE | libc::MAP_ANON;
let result = unsafe { libc::mmap(null, len, prot, flags, -1, 0) };

if result == libc::MAP_FAILED {
return Err(AllocError);
}

let addr = NonNull::new(result.cast()).ok_or(AllocError)?;
Ok(NonNull::slice_from_raw_parts(addr, size))
}

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();
_ = libc::munmap(ptr.as_ptr().cast(), size);
}
}

#[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()
);
}
}
45 changes: 17 additions & 28 deletions src/raw/alloc.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
pub(crate) use self::inner::{do_alloc, Allocator, Global};
pub(crate) use self::inner::{Allocator, Global};
use crate::alloc::alloc::Layout;
use core::ptr::NonNull;

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

// Nightly-case.
// Use unstable `allocator_api` feature.
// This is compatible with `allocator-api2` which can be enabled or not.
// This is used when building for `std`.
#[cfg(feature = "nightly")]
mod inner {
use crate::alloc::alloc::Layout;
pub use crate::alloc::alloc::{Allocator, Global};
use core::ptr::NonNull;

#[allow(clippy::map_err_ignore)]
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()),
Err(_) => Err(()),
}
}
}

// Basic non-nightly case.
Expand All @@ -27,17 +27,7 @@ mod inner {
// `core::alloc::Allocator`.
#[cfg(all(not(feature = "nightly"), feature = "allocator-api2"))]
mod inner {
use crate::alloc::alloc::Layout;
pub use allocator_api2::alloc::{Allocator, Global};
use core::ptr::NonNull;

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

// No-defaults case.
Expand All @@ -55,7 +45,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 @@ -64,8 +54,11 @@ 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(ptr) => Ok(NonNull::slice_from_raw_parts(ptr, layout.size())),
None => Err(()),
}
}
#[inline]
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
Expand All @@ -79,8 +72,4 @@ mod inner {
Global
}
}

pub(crate) fn do_alloc<A: Allocator>(alloc: &A, layout: Layout) -> Result<NonNull<u8>, ()> {
alloc.allocate(layout)
}
}
Loading
Loading