feat: introduce stack-allocated PyBuffer#5894
Conversation
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks very much for this. Various thoughts around the accessor methods.
Also, out of scope for this PR, but I keep wondering if we should provide iterators for these structures. Especially with the strides / suboffsets etc, it's not necessarily trivial to get this right.
| /// Gets the size of a single element, in bytes. | ||
| #[inline] | ||
| pub fn item_size(&self) -> usize { | ||
| self.raw.itemsize as usize |
There was a problem hiding this comment.
Interesting note from the Python docs for itemsize
If shape is NULL as a result of a PyBUF_SIMPLE or a PyBUF_WRITABLE request, the consumer must disregard itemsize and assume itemsize == 1.
I am unsure whether it's better to have a corresponding check for shape here and return 1 if needed, or to return None if shape is None, or to just keep the implementation as-is and document this for users.
There was a problem hiding this comment.
I am in favour of returning 1. Also, instead of doing the NULL check inside the method, we could probably do it in with() instead so that the check is only done once.
Yes, I think it would definitely be useful as well. I am not yet sure how this would look like though. |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks for the continued work here, looking great!
Implementing Drop directly on PyUntypedBufferView is functionally equivalent to what I had in mind from the "drop guard", so gets a 👍 from me.
Afraid I had quite a few more thoughts around some of the edge cases (plus some ideas we can probably ignore). Hopefully helps us get to the right eventual abstraction!
5430e96 to
48b5fdd
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
b4f1eab to
dd63129
Compare
src/buffer.rs
Outdated
| impl_element!(f64, Float); | ||
|
|
||
| /// Sealed marker for buffer field availability. Either [`Known`] or [`Unknown`]. | ||
| mod buffer_info { |
There was a problem hiding this comment.
I wonder if this should be moved to another file or moved to the top level.
There was a problem hiding this comment.
We actually already have pyo3::pyclass::boolean_struct::True / False which could potentially be deduplicated with these types.
It might be possible to also use const FORMAT: bool etc as const-generics instead of types. Not sure if that offers any practical benefit here. I think it's not possible to provide defaults for const generics, so it might work out strictly worse I guess?
There was a problem hiding this comment.
It might be possible to also use const FORMAT: bool etc as const-generics instead of types.
You're right. It works, and it's a lot cleaner. It's possible to set defaults for const generics.
davidhewitt
left a comment
There was a problem hiding this comment.
Very cool!
After reading through this implementation a couple of times, I really like the expressiveness this gives in the type system. I have some ideas which might refine it further (see BufferFlags struct idea).
Main concern is about generic code bloat from explosion of generic parameters. I am unsure if there's a way that we can mitigate that at all.
src/buffer.rs
Outdated
| impl_element!(f64, Float); | ||
|
|
||
| /// Sealed marker for buffer field availability. Either [`Known`] or [`Unknown`]. | ||
| mod buffer_info { |
There was a problem hiding this comment.
We actually already have pyo3::pyclass::boolean_struct::True / False which could potentially be deduplicated with these types.
It might be possible to also use const FORMAT: bool etc as const-generics instead of types. Not sure if that offers any practical benefit here. I think it's not possible to provide defaults for const generics, so it might work out strictly worse I guess?
src/buffer.rs
Outdated
| impl<Format: FieldInfo, Stride: FieldInfo> PyUntypedBufferView<Format, Known, Stride> { | ||
| /// Returns the shape array. `shape[i]` is the length of dimension `i`. | ||
| /// | ||
| /// Despite Python using an array of signed integers, the values are guaranteed to be | ||
| /// non-negative. However, dimensions of length 0 are possible and might need special | ||
| /// attention. | ||
| #[inline] | ||
| pub fn shape(&self) -> &[usize] { | ||
| debug_assert!(!self.raw.shape.is_null()); | ||
| unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) } | ||
| } | ||
| } |
There was a problem hiding this comment.
I think it's potentially also valid to have PyUntypedBufferView<Format, Unknown, Stride> implement a shape() accessor which returns Option<&[usize]>. I am unsure if that's of any practical value, it seems to me that it would be expected to always return None.
(Maybe similar observation for format / stride parameters.)
There was a problem hiding this comment.
I can't see this being useful, and I can't think of a good way to implement this. I am assuming this is necessary for someone who wants to resolve the flags dynamically at runtime? I don’t think it’s worth supporting that. Such users should just request for all the flags.
src/buffer.rs
Outdated
| #[repr(transparent)] | ||
| pub struct PyBufferView< | ||
| T, | ||
| Format: FieldInfo = Known, |
There was a problem hiding this comment.
I suppose the typed buffer view always needs to know format, this parameter might not be necessary (however I guess is useful for consistency).
There was a problem hiding this comment.
We'd have to add another flag type, which isn't worth it imo.
| /// Attempt to interpret this untyped view as containing elements of type `T`. | ||
| pub fn as_typed<T: Element>(&self) -> PyResult<&PyBufferView<T, Known, Shape, Stride>> { | ||
| self.ensure_compatible_with::<T>()?; | ||
| // SAFETY: PyBufferView<T, ..> is repr(transparent) around PyUntypedBufferView<..> | ||
| let typed = unsafe { | ||
| NonNull::from(self) | ||
| .cast::<PyBufferView<T, Known, Shape, Stride>>() | ||
| .as_ref() | ||
| }; | ||
|
|
||
| Ok(typed) | ||
| } | ||
|
|
||
| fn ensure_compatible_with<T: Element>(&self) -> PyResult<()> { | ||
| let name = std::any::type_name::<T>(); | ||
|
|
||
| if mem::size_of::<T>() != self.item_size() || !T::is_compatible_format(self.format()) { | ||
| return Err(PyBufferError::new_err(format!( | ||
| "buffer contents are not compatible with {name}" | ||
| ))); | ||
| } | ||
|
|
||
| if self.raw.buf.align_offset(mem::align_of::<T>()) != 0 { | ||
| return Err(PyBufferError::new_err(format!( | ||
| "buffer contents are insufficiently aligned for {name}" | ||
| ))); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
One downside of the high numbers of generic parameters is that these methods are monomorphised many times over during codegen. We might want to have fn inner internal methods which are only generic on T for these larger methods. Small ones probably aren't worth applying such complexity for, I don't have a good instinct for a cut-off threshold on that.
There was a problem hiding this comment.
Most of the methods are pretty short. I guess ensure_compatible_with is a good candidate?
|
Thanks for all of this - would be interested to see what you think of these suggestions (none are mandatory, I'm just pushing out ideas of what I think I like, which may not be to others' taste!) |
|
#5870 has now merged, we'll want to add a similar API here. |
Co-authored-by: David Hewitt <[email protected]>
dd63129 to
5d3c801
Compare
5d3c801 to
c9e582a
Compare
a462497 to
fcb9203
Compare
fcb9203 to
c1872f3
Compare
|
Sorry for the delay. I've been a little burnt out from school + work. I've adapted your suggestions and modified them to be what I think is appropriate. Also, if you do |
Summary
This PR implements a pinned stack-allocated
PyUntypedBuffervariant,PyUntypedBufferView.Closes #5836