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

build compatibility and thread sharing #191

Merged
merged 14 commits into from
Dec 6, 2024

Conversation

ryancinsight
Copy link
Contributor

changes to builds as cmake and cc were failing windows gnu, clang, ucrt, but now working

@ryancinsight
Copy link
Contributor Author

changes to builds as cmake and cc were failing windows gnu, clang, ucrt, but now working

Seems I need to recheck build_cc for other os versions. I'll update over next day

@SchrodingerZhu
Copy link
Owner

FYI, FreeBSD is of LLVM-like toolchain.

@ryancinsight
Copy link
Contributor Author

FYI, FreeBSD is of LLVM-like toolchain.

thanks so much, I am not used to this one!

In testing I noticed an almost doubling of test time between build_cc and cmake, they are now unified to make sure there is reduced chance of difference between builds. also enabled ability to read build environment of allocator
@ryancinsight
Copy link
Contributor Author

@SchrodingerZhu how do you normally test on freebsd locally? do you have Dockerfile?

@SchrodingerZhu
Copy link
Owner

This builder cleanup is something I have been always wanting to do. Thanks a lot! I will take a look tomorrow (EDT).

@ryancinsight
Copy link
Contributor Author

This builder cleanup is something I have been always wanting to do. Thanks a lot! I will take a look tomorrow (EDT).

I'm debating redoing again but with greater focus on compiler toolchain but this was already gnarly and more than suitable for my needs so I may wait a bit. At least now the builds are appearing the same regardless of build option unlike before. I also made changes to lib.rs in snmalloc_rs as I'm trying to share allocator itself over a new process or thread and hijack memory calls

@SchrodingerZhu
Copy link
Owner

SchrodingerZhu commented Dec 3, 2024

Can u illustrate more on why you have added special handling of zero-sized layout? The rust document says it is up to the user to make sure the layout is of nonzero size.

Extension subtraits might provide more specific bounds on behavior, e.g., guarantee a sentinel address or a null pointer in response to a zero-size allocation request.

I think sentinel value is a good practice, but I personally prefer to provide another struct (Say SentineledSnMalloc) that wraps up the SnMalloc struct and adds additional check before calling into inner layer.

@ryancinsight
Copy link
Contributor Author

Can u illustrate more on why you have added special handling of zero-sized layout? The rust document says it is up to the user to make sure the layout is of nonzero size.

Extension subtraits might provide more specific bounds on behavior, e.g., guarantee a sentinel address or a null pointer in response to a zero-size allocation request.

I think sentinel value is a good practice, but I personally prefer to provide another struct (Say SentineledSnMalloc) that wraps up the SnMalloc struct and adds additional check before calling into inner layer.

yeah, I saw that in the rust manual but I looked further and found that by explicitly handling zero-sized layouts, we can prevent undefined behavior by 1) Returning an aligned non-null pointer for zero-sized allocations without actually allocating memory 2) Skipping deallocation calls for zero-sized types 3) Handling reallocation edge cases involving zero sizes. Overall, the major benefit should be from zero-sized types (ZSTs) which can be optimized to no ops without unnecessary allocation calls

snmalloc-sys/build.rs Outdated Show resolved Hide resolved
snmalloc-sys/build.rs Outdated Show resolved Hide resolved
@SchrodingerZhu
Copy link
Owner

SchrodingerZhu commented Dec 3, 2024

Can u illustrate more on why you have added special handling of zero-sized layout? The rust document says it is up to the user to make sure the layout is of nonzero size.

Extension subtraits might provide more specific bounds on behavior, e.g., guarantee a sentinel address or a null pointer in response to a zero-size allocation request.

I think sentinel value is a good practice, but I personally prefer to provide another struct (Say SentineledSnMalloc) that wraps up the SnMalloc struct and adds additional check before calling into inner layer.

yeah, I saw that in the rust manual but I looked further and found that by explicitly handling zero-sized layouts, we can prevent undefined behavior by 1) Returning an aligned non-null pointer for zero-sized allocations without actually allocating memory 2) Skipping deallocation calls for zero-sized types 3) Handling reallocation edge cases involving zero sizes. Overall, the major benefit should be from zero-sized types (ZSTs) which can be optimized to no ops without unnecessary allocation calls

Solid.

Given that in most allocation site the size info is statically known, I think LTO will remove this branch. @ryancinsight can you justify this?


Oh, you actually mark the function as inline(always). It should be fine then.

Copy link
Owner

@SchrodingerZhu SchrodingerZhu left a comment

Choose a reason for hiding this comment

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

Rest parts look good to me but please address @devnexen's comment. Thank you for the patch!

@ryancinsight
Copy link
Contributor Author

Rest parts look good to me but please address @devnexen's comment. Thank you for the patch!

sorry for delay, didn't like the skipping of windows for LTO and linking differences between systems were giving some issues so now revised and working as far as I can tell

snmalloc-sys/build.rs Outdated Show resolved Hide resolved
@SchrodingerZhu SchrodingerZhu merged commit c9d63b2 into SchrodingerZhu:master Dec 6, 2024
9 checks passed
@ryancinsight
Copy link
Contributor Author

Can u illustrate more on why you have added special handling of zero-sized layout? The rust document says it is up to the user to make sure the layout is of nonzero size.

Extension subtraits might provide more specific bounds on behavior, e.g., guarantee a sentinel address or a null pointer in response to a zero-size allocation request.

I think sentinel value is a good practice, but I personally prefer to provide another struct (Say SentineledSnMalloc) that wraps up the SnMalloc struct and adds additional check before calling into inner layer.

Are you thinking something like this, still have some redundancies but can clean up:

``#![no_std]
//! snmalloc-rs provides a wrapper for `microsoft/snmalloc` to make it usable as a global allocator for rust.
//! snmalloc is a research allocator. Its key design features are:
//! - Memory that is freed by the same thread that allocated it does not require any synchronising operations.
//! - Freeing memory in a different thread to initially allocated it, does not take any locks and instead uses a novel message passing scheme to return the memory to the original allocator, where it is recycled.
//! - The allocator uses large ranges of pages to reduce the amount of meta-data required.
//!
//! The benchmark is available at the paper of `snmalloc`
//! There are three features defined in this crate:
//! - `debug`: Enable the `Debug` mode in `snmalloc`.
//! - `1mib`: Use the `1mib` chunk configuration.
//! - `cache-friendly`: Make the allocator more cache friendly (setting `CACHE_FRIENDLY_OFFSET` to `64` in building the library).
//!
//! The whole library supports `no_std`.
//!
//! To use `snmalloc-rs` add it as a dependency:
//! toml //! # Cargo.toml //! [dependencies] //! snmalloc-rs = "0.1.0" //!
//!
//! To set `SnMalloc` as the global allocator add this to your project:
//! ```rust
//! #[global_allocator]
//! static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc;
//! ```
extern crate snmalloc_sys as ffi;

use core::{
alloc::{GlobalAlloc, Layout},
ptr::NonNull,
};

#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct SnMalloc;

unsafe impl Send for SnMalloc {}
unsafe impl Sync for SnMalloc {}

impl SnMalloc {
#[inline(always)]
pub const fn new() -> Self {
Self
}
#[inline(always)]
pub fn safe_alloc(&self, layout: Layout) -> Option<NonNull> {
if layout.size() == 0 {
Some(NonNull::dangling()) // Use NonNull::dangling for ZSTs
} else {
NonNull::new(unsafe { ffi::sn_rust_alloc(layout.align(), layout.size()) }.cast())
}
}
#[inline(always)]
pub fn safe_dealloc(&self, ptr: *mut u8, layout: Layout) {
// Ensure the pointer is not null and size is non-zero
if !ptr.is_null() && layout.size() != 0 {
unsafe {
ffi::sn_rust_dealloc(ptr.cast(), layout.align(), layout.size());
}
}
// Zero-sized allocations do not require deallocation.
}

#[inline(always)]
pub fn safe_alloc_zeroed(&self, layout: Layout) -> Option<NonNull<u8>> {
    if layout.size() == 0 {
        Some(NonNull::dangling()) // Use NonNull::dangling for zero-sized allocations
    } else {
        NonNull::new(unsafe { ffi::sn_rust_alloc_zeroed(layout.align(), layout.size()) }.cast())
    }
}

#[inline(always)]
pub fn safe_realloc(
    &self,
    ptr: *mut u8,
    layout: Layout,
    new_size: usize,
) -> Option<NonNull<u8>> {
    match (layout.size(), new_size) {
        // Both old and new sizes are zero; return a sentinel value
        (0, 0) => Some(NonNull::dangling()),

        // Old size is zero; allocate new memory
        (0, _) => self.alloc_aligned(Layout::from_size_align(new_size, layout.align()).ok()?),

        // New size is zero; deallocate and return None
        (_, 0) => {
            self.safe_dealloc(ptr, layout);
            None
        }

        // Delegate to snmalloc for reallocation
        _ => NonNull::new(unsafe {
            ffi::sn_rust_realloc(ptr.cast(), layout.align(), layout.size(), new_size).cast()
        }),
    }
}

/// Returns the available bytes in a memory block.
#[inline(always)]
pub fn usable_size(&self, ptr: *const u8) -> Option<usize> {
    match ptr.is_null() {
        true => None,
        false => Some(unsafe { ffi::sn_rust_usable_size(ptr.cast()) })
    }
}

/// Allocates memory with the given layout, returning a non-null pointer on success
#[inline(always)]
pub fn alloc_aligned(&self, layout: Layout) -> Option<NonNull<u8>> {
    match layout.size() {
        0 => Some(NonNull::dangling()),
        size => NonNull::new(unsafe { ffi::sn_rust_alloc(layout.align(), size) }.cast())
    }
}

}

unsafe impl GlobalAlloc for SnMalloc {
/// Allocate the memory with the given alignment and size.
/// On success, it returns a pointer pointing to the required memory address.
/// On failure, it returns a null pointer.
/// The client must assure the following things:
/// - alignment is greater than zero
/// - Other constrains are the same as the rust standard library.
///
/// The program may be forced to abort if the constrains are not full-filled.
#[inline(always)]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
self.safe_alloc(layout).map_or(core::ptr::null_mut(), |ptr| ptr.as_ptr())
}

/// De-allocate the memory at the given address with the given alignment and size.
/// The client must assure the following things:
/// - the memory is acquired using the same allocator and the pointer points to the start position.
/// - Other constrains are the same as the rust standard library.
///
/// The program may be forced to abort if the constrains are not full-filled.
#[inline(always)]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    self.safe_dealloc(ptr, layout);
}

/// Behaves like alloc, but also ensures that the contents are set to zero before being returned.
#[inline(always)]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
    self.safe_alloc_zeroed(layout).map_or(core::ptr::null_mut(), |ptr| ptr.as_ptr())
}

/// Re-allocate the memory at the given address with the given alignment and size.
/// On success, it returns a pointer pointing to the required memory address.
/// The memory content within the `new_size` will remains the same as previous.
/// On failure, it returns a null pointer. In this situation, the previous memory is not returned to the allocator.
/// The client must assure the following things:
/// - the memory is acquired using the same allocator and the pointer points to the start position
/// - `alignment` fulfills all the requirements as `rust_alloc`
/// - Other constrains are the same as the rust standard library.
///
/// The program may be forced to abort if the constrains are not full-filled.
#[inline(always)]
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
    self.safe_realloc(ptr, layout, new_size)
        .map_or(core::ptr::null_mut(), |ptr| ptr.as_ptr())
}

}

/// Wrapper around SnMalloc to handle zero-sized layouts explicitly.
pub struct SentineledSnMalloc {
inner: SnMalloc,
}

impl SentineledSnMalloc {
/// Creates a new instance of the sentinel wrapper.
pub const fn new() -> Self {
Self { inner: SnMalloc::new() }
}

fn handle_zero_sized_alloc(&self) -> *mut u8 {
    NonNull::dangling().as_ptr()
}

}

unsafe impl GlobalAlloc for SentineledSnMalloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if layout.size() == 0 {
self.handle_zero_sized_alloc()
} else {
self.inner.alloc(layout)
}
}

unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    if layout.size() != 0 {
        self.inner.dealloc(ptr, layout);
    }
    // Zero-sized allocations do not require deallocation.
}

unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
    if layout.size() == 0 {
        self.handle_zero_sized_alloc()
    } else {
        self.inner.alloc_zeroed(layout)
    }
}

unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
    if new_size == 0 {
        self.dealloc(ptr, layout);
        self.handle_zero_sized_alloc()
    } else if layout.size() == 0 {
        self.alloc(Layout::from_size_align_unchecked(new_size, layout.align()))
    } else {
        ffi::sn_rust_realloc(ptr.cast(), layout.align(), layout.size(), new_size).cast()
    }
}

}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allocation_lifecycle() {
let alloc = SentineledSnMalloc::new();
unsafe {
let layout = Layout::from_size_align(8, 8).unwrap();

        // Test regular allocation
        let ptr = alloc.alloc(layout);
        alloc.dealloc(ptr, layout);

        // Test zeroed allocation
        let ptr = alloc.alloc_zeroed(layout);
        alloc.dealloc(ptr, layout);

        // Test reallocation
        let ptr = alloc.alloc(layout);
        let ptr = alloc.realloc(ptr, layout, 16);
        alloc.dealloc(ptr, layout);

        // Test large allocation
        let large_layout = Layout::from_size_align(1 << 20, 32).unwrap();
        let ptr = alloc.alloc(large_layout);
        alloc.dealloc(ptr, large_layout);
    }
}
#[test]
fn it_frees_allocated_memory() {
    let alloc = SentineledSnMalloc::new();
    unsafe {
        let layout = Layout::from_size_align(8, 8).unwrap();
        let ptr = alloc.alloc(layout);
        alloc.dealloc(ptr, layout);
    }
}

#[test]
fn it_frees_zero_allocated_memory() {
    let alloc = SentineledSnMalloc::new();
    unsafe {
        let layout = Layout::from_size_align(8, 8).unwrap();

        let ptr = alloc.alloc_zeroed(layout);
        alloc.dealloc(ptr, layout);
    }
}

#[test]
fn it_frees_reallocated_memory() {
    let alloc = SentineledSnMalloc::new();
    unsafe {
        let layout = Layout::from_size_align(8, 8).unwrap();

        let ptr = alloc.alloc(layout);
        let ptr = alloc.realloc(ptr, layout, 16);
        alloc.dealloc(ptr, layout);
    }
}

#[test]
fn it_frees_large_alloc() {
    let alloc = SentineledSnMalloc::new();
    unsafe {
        let layout = Layout::from_size_align(1 << 20, 32).unwrap();

        let ptr = alloc.alloc(layout);
        alloc.dealloc(ptr, layout);
    }
}

#[test]
fn test_usable_size() {
    let alloc = SentineledSnMalloc::new();
    unsafe {
        let layout = Layout::from_size_align(8, 8).unwrap();
        let ptr = alloc.alloc(layout);
        let usz = alloc.inner.usable_size(ptr).expect("usable_size returned None");
        alloc.dealloc(ptr, layout);
        assert!(usz >= 8);
    }
}

}
``

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants