Skip to content

Switch Buffers to memoryviews & remove extra copies/allocations #656

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

Merged
merged 31 commits into from
Mar 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0c823e2
Switch `Buffer`s to `memoryview`s
jakirkham Nov 22, 2024
c690446
Merge zarr-developers/main into jakirkham/use_mv
jakirkham Mar 26, 2025
9ac8d8e
Add back `ZSTD_freeCCtx`
jakirkham Mar 26, 2025
0ade20c
Drop leftover `Buffer` from merge conflict
jakirkham Mar 26, 2025
13f8e50
Add minor comment
jakirkham Mar 26, 2025
bd1c401
Add trivial `try...finally...`s to cleanup diff
jakirkham Mar 26, 2025
64eed12
Use Cython `cimport`s for Python C API
jakirkham Mar 26, 2025
31a7446
Add news entry
jakirkham Mar 26, 2025
9069553
Move `cimport`s from `libc` up top
jakirkham Mar 26, 2025
1ab9983
Resize buffers without copying
jakirkham Mar 26, 2025
f91c283
Write directly to output array in VLen*
jakirkham Mar 26, 2025
d0a7721
Use `_mv` subscript name for typed-memoryivews
jakirkham Mar 26, 2025
7bc8022
Avoid excess copies in fletcher32
jakirkham Mar 26, 2025
4341ce3
Add news entry for better memory usage
jakirkham Mar 26, 2025
4baf1e1
Fix fletecher32's `cimport`s
jakirkham Mar 26, 2025
0ea019c
Fix blank lines to match
jakirkham Mar 26, 2025
2b871c7
Reassign `dest` with `dest_objptr`
jakirkham Mar 26, 2025
bdc7bc9
Fix `source_ptr` type
jakirkham Mar 26, 2025
0bfafed
Fix declaration order
jakirkham Mar 26, 2025
c17e314
Wrap `_PyBytes_Resize` for improved usability
jakirkham Mar 27, 2025
54fd4b2
Add `ensure_continguous_memoryview` function
jakirkham Mar 27, 2025
eac4ef1
Use `ensure_contiguous_memoryview`
jakirkham Mar 27, 2025
a77e8cf
Move `PyBytes_RESIZE` macro to `compat_ext`
jakirkham Mar 27, 2025
fb5ffac
Minimize diff by readding blank line after `try`
jakirkham Mar 27, 2025
9b289c6
Group `encv` with `value` args
jakirkham Mar 27, 2025
7d593b8
Organize `vlen`'s `imports`
jakirkham Mar 27, 2025
7d68d53
Use `ensure_contiguous_memoryview` with VLen
jakirkham Mar 27, 2025
b6d91ce
Space out input arg handling & checksum check
jakirkham Mar 27, 2025
2dd1cdf
Use `memcpy` to speedup copies in `fletcher32`
jakirkham Mar 27, 2025
b7bb7ef
In fletcher32's if output buffer, slice from input
jakirkham Mar 27, 2025
c83136a
Unwrap lines no longer needing wrapping
jakirkham Mar 27, 2025
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
7 changes: 7 additions & 0 deletions docs/release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ Fixes
~~~~~
* Remove redundant ``id`` from codec metadata serialization in Zarr3 codecs.
By :user:`Norman Rzepka <normanrz>`, :issue:`685`
* Preallocate output buffers and resize directly as needed.
By :user:`John Kirkham <jakirkham>`, :issue:`656`

Maintenance
~~~~~~~~~~~
* Replace internal ``Buffer`` usage with ``memoryview``\ s.
By :user:`John Kirkham <jakirkham>`, :issue:`656`

.. _release_0.15.0:

Expand Down
155 changes: 81 additions & 74 deletions numcodecs/blosc.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import os
from deprecated import deprecated


from cpython.buffer cimport PyBUF_ANY_CONTIGUOUS, PyBUF_WRITEABLE
from cpython.bytes cimport PyBytes_FromStringAndSize, PyBytes_AS_STRING
from cpython.bytes cimport PyBytes_AS_STRING, PyBytes_FromStringAndSize
from cpython.memoryview cimport PyMemoryView_GET_BUFFER

from .compat_ext cimport PyBytes_RESIZE, ensure_continguous_memoryview

from .compat_ext cimport Buffer
from .compat_ext import Buffer
from .compat import ensure_contiguous_ndarray
from .abc import Codec

Expand Down Expand Up @@ -154,17 +153,16 @@ def _cbuffer_sizes(source):

"""
cdef:
Buffer buffer
memoryview source_mv
const Py_buffer* source_pb
size_t nbytes, cbytes, blocksize

# obtain buffer
buffer = Buffer(source, PyBUF_ANY_CONTIGUOUS)
# obtain source memoryview
source_mv = ensure_continguous_memoryview(source)
source_pb = PyMemoryView_GET_BUFFER(source_mv)

# determine buffer size
blosc_cbuffer_sizes(buffer.ptr, &nbytes, &cbytes, &blocksize)

# release buffers
buffer.release()
blosc_cbuffer_sizes(source_pb.buf, &nbytes, &cbytes, &blocksize)

return nbytes, cbytes, blocksize

Expand All @@ -173,16 +171,15 @@ cbuffer_sizes = deprecated(_cbuffer_sizes)
def cbuffer_complib(source):
"""Return the name of the compression library used to compress `source`."""
cdef:
Buffer buffer
memoryview source_mv
const Py_buffer* source_pb

# obtain buffer
buffer = Buffer(source, PyBUF_ANY_CONTIGUOUS)
# obtain source memoryview
source_mv = ensure_continguous_memoryview(source)
source_pb = PyMemoryView_GET_BUFFER(source_mv)

# determine buffer size
complib = blosc_cbuffer_complib(buffer.ptr)

# release buffers
buffer.release()
complib = blosc_cbuffer_complib(source_pb.buf)

complib = complib.decode('ascii')

Expand All @@ -202,18 +199,17 @@ def _cbuffer_metainfo(source):

"""
cdef:
Buffer buffer
memoryview source_mv
const Py_buffer* source_pb
size_t typesize
int flags

# obtain buffer
buffer = Buffer(source, PyBUF_ANY_CONTIGUOUS)
# obtain source memoryview
source_mv = ensure_continguous_memoryview(source)
source_pb = PyMemoryView_GET_BUFFER(source_mv)

# determine buffer size
blosc_cbuffer_metainfo(buffer.ptr, &typesize, &flags)

# release buffers
buffer.release()
blosc_cbuffer_metainfo(source_pb.buf, &typesize, &flags)

# decompose flags
if flags & BLOSC_DOSHUFFLE:
Expand Down Expand Up @@ -263,28 +259,34 @@ def compress(source, char* cname, int clevel, int shuffle=SHUFFLE,
"""

cdef:
char *source_ptr
char *dest_ptr
Buffer source_buffer
memoryview source_mv
const Py_buffer* source_pb
const char* source_ptr
size_t nbytes, itemsize
int cbytes
bytes dest
char* dest_ptr

# check valid cname early
cname_str = cname.decode('ascii')
if cname_str not in list_compressors():
_err_bad_cname(cname_str)

# setup source buffer
source_buffer = Buffer(source, PyBUF_ANY_CONTIGUOUS)
source_ptr = source_buffer.ptr
nbytes = source_buffer.nbytes
# obtain source memoryview
source_mv = ensure_continguous_memoryview(source)
source_pb = PyMemoryView_GET_BUFFER(source_mv)

# extract metadata
source_ptr = <const char*>source_pb.buf
nbytes = source_pb.len

# validate typesize
if isinstance(typesize, int):
if typesize < 1:
raise ValueError(f"Cannot use typesize {typesize} less than 1.")
itemsize = typesize
else:
itemsize = source_buffer.itemsize
itemsize = source_pb.itemsize

# determine shuffle
if shuffle == AUTOSHUFFLE:
Expand Down Expand Up @@ -333,16 +335,14 @@ def compress(source, char* cname, int clevel, int shuffle=SHUFFLE,
cname, blocksize, 1)

finally:

# release buffers
source_buffer.release()
pass

# check compression was successful
if cbytes <= 0:
raise RuntimeError('error during blosc compression: %d' % cbytes)

# resize after compression
dest = dest[:cbytes]
PyBytes_RESIZE(dest, cbytes)

return dest

Expand All @@ -366,30 +366,36 @@ def decompress(source, dest=None):
"""
cdef:
int ret
char *source_ptr
char *dest_ptr
Buffer source_buffer
Buffer dest_buffer = None
memoryview source_mv
const Py_buffer* source_pb
const char* source_ptr
memoryview dest_mv
Py_buffer* dest_pb
char* dest_ptr
size_t nbytes, cbytes, blocksize

# setup source buffer
source_buffer = Buffer(source, PyBUF_ANY_CONTIGUOUS)
source_ptr = source_buffer.ptr
# obtain source memoryview
source_mv = ensure_continguous_memoryview(source)
source_pb = PyMemoryView_GET_BUFFER(source_mv)

# get source pointer
source_ptr = <const char*>source_pb.buf

# determine buffer size
blosc_cbuffer_sizes(source_ptr, &nbytes, &cbytes, &blocksize)

# setup destination buffer
if dest is None:
# allocate memory
dest = PyBytes_FromStringAndSize(NULL, nbytes)
dest_ptr = PyBytes_AS_STRING(dest)
dest_nbytes = nbytes
dest_1d = dest = PyBytes_FromStringAndSize(NULL, nbytes)
else:
arr = ensure_contiguous_ndarray(dest)
dest_buffer = Buffer(arr, PyBUF_ANY_CONTIGUOUS | PyBUF_WRITEABLE)
dest_ptr = dest_buffer.ptr
dest_nbytes = dest_buffer.nbytes
dest_1d = ensure_contiguous_ndarray(dest)

# obtain dest memoryview
dest_mv = memoryview(dest_1d)
dest_pb = PyMemoryView_GET_BUFFER(dest_mv)
dest_ptr = <char*>dest_pb.buf
dest_nbytes = dest_pb.len

try:

Expand All @@ -408,11 +414,7 @@ def decompress(source, dest=None):
ret = blosc_decompress_ctx(source_ptr, dest_ptr, nbytes, 1)

finally:

# release buffers
source_buffer.release()
if dest_buffer is not None:
dest_buffer.release()
pass

# handle errors
if ret <= 0:
Expand Down Expand Up @@ -449,14 +451,20 @@ def _decompress_partial(source, start, nitems, dest=None):
int encoding_size
int nitems_bytes
int start_bytes
char *source_ptr
char *dest_ptr
Buffer source_buffer
Buffer dest_buffer = None
memoryview source_mv
const Py_buffer* source_pb
const char* source_ptr
memoryview dest_mv
Py_buffer* dest_pb
char* dest_ptr
size_t dest_nbytes

# setup source buffer
source_buffer = Buffer(source, PyBUF_ANY_CONTIGUOUS)
source_ptr = source_buffer.ptr
# obtain source memoryview
source_mv = ensure_continguous_memoryview(source)
source_pb = PyMemoryView_GET_BUFFER(source_mv)

# setup source pointer
source_ptr = <const char*>source_pb.buf

# get encoding size from source buffer header
encoding_size = source[3]
Expand All @@ -467,26 +475,25 @@ def _decompress_partial(source, start, nitems, dest=None):

# setup destination buffer
if dest is None:
dest = PyBytes_FromStringAndSize(NULL, nitems_bytes)
dest_ptr = PyBytes_AS_STRING(dest)
dest_nbytes = nitems_bytes
# allocate memory
dest_1d = dest = PyBytes_FromStringAndSize(NULL, nitems_bytes)
else:
arr = ensure_contiguous_ndarray(dest)
dest_buffer = Buffer(arr, PyBUF_ANY_CONTIGUOUS | PyBUF_WRITEABLE)
dest_ptr = dest_buffer.ptr
dest_nbytes = dest_buffer.nbytes
dest_1d = ensure_contiguous_ndarray(dest)

# obtain dest memoryview
dest_mv = memoryview(dest_1d)
dest_pb = PyMemoryView_GET_BUFFER(dest_mv)
dest_ptr = <char*>dest_pb.buf
dest_nbytes = dest_pb.len

# try decompression
try:
if dest_nbytes < nitems_bytes:
raise ValueError('destination buffer too small; expected at least %s, '
'got %s' % (nitems_bytes, dest_nbytes))
ret = blosc_getitem(source_ptr, start, nitems, dest_ptr)

finally:
source_buffer.release()
if dest_buffer is not None:
dest_buffer.release()
pass

# ret refers to the number of bytes returned from blosc_getitem.
if ret <= 0:
Expand Down
17 changes: 8 additions & 9 deletions numcodecs/compat_ext.pxd
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# cython: language_level=3


cdef class Buffer:
cdef:
char *ptr
Py_buffer buffer
size_t nbytes
size_t itemsize
bint acquired

cpdef release(self)
cdef extern from *:
"""
#define PyBytes_RESIZE(b, n) _PyBytes_Resize(&b, n)
"""
Comment on lines +4 to +7
Copy link
Member Author

Choose a reason for hiding this comment

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

This should have an #ifndef guard to protect against redefinition error

So far it seems Cython doesn't encounter this error as it defines this once per module. Still it is a good idea to add the guard

Handling in PR: #732

int PyBytes_RESIZE(object b, Py_ssize_t n) except -1
Comment on lines +4 to +8
Copy link
Member Author

@jakirkham jakirkham Mar 27, 2025

Choose a reason for hiding this comment

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

Have added this function (macro) to help with resizing bytes objects in Cython

It is a thin wrapper around _PyBytes_Resize, which makes it easier to work with in Cython

This allows us to in-place truncate bytes allocations that are larger than we end up needing without needing to copy to a new bytes object



cpdef memoryview ensure_continguous_memoryview(obj)
31 changes: 11 additions & 20 deletions numcodecs/compat_ext.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,17 @@
# cython: linetrace=False
# cython: binding=False
# cython: language_level=3
from cpython.buffer cimport PyObject_GetBuffer, PyBuffer_Release

from cpython.buffer cimport PyBuffer_IsContiguous
from cpython.memoryview cimport PyMemoryView_GET_BUFFER

from .compat import ensure_contiguous_ndarray


cdef class Buffer:
"""Convenience class for buffer interface."""

def __cinit__(self, obj, int flags):
PyObject_GetBuffer(obj, &(self.buffer), flags)
self.acquired = True
self.ptr = <char *> self.buffer.buf
self.itemsize = self.buffer.itemsize
self.nbytes = self.buffer.len

cpdef release(self):
if self.acquired:
PyBuffer_Release(&(self.buffer))
self.acquired = False

def __dealloc__(self):
self.release()
cpdef memoryview ensure_continguous_memoryview(obj):
cdef memoryview mv
if type(obj) is memoryview:
mv = <memoryview>obj
else:
mv = memoryview(obj)
if not PyBuffer_IsContiguous(PyMemoryView_GET_BUFFER(mv), b'A'):
raise BufferError("Expected contiguous memory")
return mv
Loading
Loading