-
Notifications
You must be signed in to change notification settings - Fork 1k
add PyMemRawAllocator
#6279
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
base: main
Are you sure you want to change the base?
add PyMemRawAllocator
#6279
Changes from 1 commit
56ff758
27eab03
6c554a0
781b2c3
eadec33
707c873
0ae37cc
c430046
d5dc676
56dce48
2f1bdba
8a1777c
583d780
61f76db
e3806cb
7ec0df4
bcc71cb
e388cec
d53be71
24e5953
ddf34d8
81ed81c
0aa7a07
429bb8a
1c9b8a3
e416c10
6624118
4cc1242
4c5106e
89b135c
16e9763
cffcdf7
d9f21b8
f8dd183
ea4c350
e56c4a1
db5e086
5068b35
eec4e73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| // TODO https://github.com/PyO3/pyo3/issues/5487 | ||
| #![allow(clippy::undocumented_unsafe_blocks)] | ||
|
|
||
| //! GlobalAlloc backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`). | ||
| //! | ||
| //! ``` | ||
| //! use pyo3::pymem_alloc::PyMemRawAllocator; | ||
| //! | ||
| //! #[global_allocator] | ||
| //! static GLOBAL_ALLOCATOR: PyMemRawAllocator = PyMemRawAllocator; | ||
| //! ``` | ||
|
|
||
| use core::{ | ||
| alloc::{GlobalAlloc, Layout}, | ||
| mem::size_of, | ||
| ptr, | ||
| }; | ||
|
|
||
| /// `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` functions | ||
| /// (`PYMEM_DOMAIN_RAW`). Safe to use from any thread, attached or not, | ||
| /// since the raw domain doesn't require an attached thread state. | ||
| pub struct PyMemRawAllocator; | ||
|
Comment on lines
+17
to
+20
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /// `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` functions (`PYMEM_DOMAIN_RAW`).
-/// Safe to use from any thread, attached or not, since the raw domain doesn't require an attached
-/// thread state.
+///
+/// The raw domain does not require an attached thread state.
+///
+/// If `tracemalloc` is enabled, CPython replaces the raw allocator with hooks that call
+/// `PyGILState_Ensure()` before recording a traceback, then walk the current thread's Python
+/// frame stack. Calling this allocator (`alloc`/`alloc_zeroed`/`realloc`, not `dealloc`) from a
+/// detached thread will therefore attach a thread state on demand while `tracemalloc` is tracing.
pub struct PyMemRawAllocator;
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks this looks good but maybe "attach to the thread" or "attach to Python" are more familiar to pyo3 developers then gilensure. This is editorial but it means it could be worth pointing out that libraries that allocate frequently on non-Python threads may want to only use this allocator when @kumaraditya303 sorry to randomly cc but wonder if you have any insight here. I have been wondering about the implications of tracemalloc on the scientific python changes. I don't think they guard on free-threaded only IIUC
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at
So it looks like raw-domain allocations attach a thread state on demand whenever If scientific-Python extensions allocate frequently from detached/non-Python threads, enabling Would appreciate a correction if there's a path I'm missing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry yeah - the reason I suggest free-threaded vs not is only because attach while costly doesn't block or potentically deadlock in most cases on free-threaded, so the risk of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The behaviour of tracemalloc is same on gil enabled and free-threaded builds. In terms of performance it is a little worse on free-threading because of contention on lock but that is unavoidable. In future I expect tracemalloc to be improved but in the mean time it has similar performance as on gil enabled builds.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering mostly about non-python threads, for example I think numpy uses PyMem_RawAlloc now on non-python compute threads. Doesn't tracemalloc cause the threads to serialize on allocations? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, it does but only when tracemalloc is enabled. In 3.15 I added a fast path to skip locking if tracemalloc is disabled which is the general case. |
||
|
|
||
| // CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T` | ||
| // (8 on Windows; autoconf-derived on Unix, not guaranteed >8 on every target/libc). | ||
| const MAX_ALIGN: usize = 8; | ||
|
chirizxc marked this conversation as resolved.
Outdated
|
||
|
|
||
| /// Bytes reserved before an over-aligned block to stash the original | ||
| /// pointer returned by `PyMem_RawMalloc`, so it can be recovered for | ||
| /// `PyMem_RawFree` / `PyMem_RawRealloc`. | ||
| const HEADER: usize = size_of::<*mut u8>(); | ||
|
|
||
| #[cold] | ||
| unsafe fn raw_alloc_aligned_with_header(layout: Layout) -> *mut u8 { | ||
| let Some(total) = layout | ||
| .size() | ||
| .checked_add(layout.align()) | ||
| .and_then(|total| total.checked_add(HEADER)) | ||
| else { | ||
| return ptr::null_mut(); | ||
| }; | ||
|
|
||
| let raw = unsafe { pyo3_ffi::PyMem_RawMalloc(total) } as *mut u8; | ||
|
|
||
| if raw.is_null() { | ||
| return ptr::null_mut(); | ||
| } | ||
|
|
||
| unsafe { finish_aligned(raw, layout) } | ||
| } | ||
|
|
||
| #[cold] | ||
| unsafe fn raw_calloc_aligned_with_header(layout: Layout) -> *mut u8 { | ||
| let Some(total) = layout | ||
| .size() | ||
| .checked_add(layout.align()) | ||
| .and_then(|total| total.checked_add(HEADER)) | ||
| else { | ||
| return ptr::null_mut(); | ||
| }; | ||
| let raw = unsafe { pyo3_ffi::PyMem_RawCalloc(1, total) } as *mut u8; | ||
|
|
||
| if raw.is_null() { | ||
| return ptr::null_mut(); | ||
| } | ||
| unsafe { finish_aligned(raw, layout) } | ||
| } | ||
|
|
||
| #[inline] | ||
| unsafe fn finish_aligned(raw: *mut u8, layout: Layout) -> *mut u8 { | ||
| let addr = raw as usize + HEADER; | ||
| let aligned_addr = (addr + layout.align() - 1) & !(layout.align() - 1); | ||
| let block = unsafe { raw.add(aligned_addr - raw as usize) }; | ||
| unsafe { (block.sub(HEADER) as *mut *mut u8).write_unaligned(raw) }; | ||
|
|
||
| block | ||
| } | ||
|
|
||
| #[inline] | ||
| unsafe fn recover_raw(ptr: *mut u8) -> *mut u8 { | ||
| unsafe { (ptr.sub(HEADER) as *mut *mut u8).read_unaligned() } | ||
| } | ||
|
|
||
| unsafe impl GlobalAlloc for PyMemRawAllocator { | ||
| unsafe fn alloc(&self, layout: Layout) -> *mut u8 { | ||
| if layout.align() <= MAX_ALIGN { | ||
| unsafe { pyo3_ffi::PyMem_RawMalloc(layout.size()) as *mut u8 } | ||
| } else { | ||
| unsafe { raw_alloc_aligned_with_header(layout) } | ||
| } | ||
| } | ||
|
|
||
| unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { | ||
| if layout.align() <= MAX_ALIGN { | ||
| unsafe { pyo3_ffi::PyMem_RawFree(ptr as *mut _) } | ||
| } else { | ||
| let raw = unsafe { recover_raw(ptr) }; | ||
| unsafe { pyo3_ffi::PyMem_RawFree(raw as *mut _) } | ||
| } | ||
| } | ||
|
|
||
| unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { | ||
| if layout.align() <= MAX_ALIGN { | ||
| unsafe { pyo3_ffi::PyMem_RawCalloc(1, layout.size()) as *mut u8 } | ||
| } else { | ||
| unsafe { raw_calloc_aligned_with_header(layout) } | ||
| } | ||
| } | ||
|
|
||
| unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { | ||
| let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; | ||
|
|
||
| if layout.align() <= MAX_ALIGN && new_layout.align() <= MAX_ALIGN { | ||
| return unsafe { pyo3_ffi::PyMem_RawRealloc(ptr as *mut _, new_size) as *mut u8 }; | ||
| } | ||
|
|
||
| let new_ptr = unsafe { self.alloc(new_layout) }; | ||
| if !new_ptr.is_null() { | ||
| unsafe { | ||
| ptr::copy_nonoverlapping(ptr, new_ptr, layout.size().min(new_size)); | ||
| self.dealloc(ptr, layout); | ||
| } | ||
| } | ||
| new_ptr | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.