Skip to content

Commit ce2a6bc

Browse files
committed
Apply review comments
1 parent a843add commit ce2a6bc

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

‎mkl/_mkl_memory.pyx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,19 @@ cdef class MKLMemory:
163163
address alignment of the allocation in bytes. Expected to be a
164164
power of two and to not exceed ``INT_MAX``. Defaults to the
165165
alignment of ``other`` in the copy form, and to `64` otherwise.
166+
167+
Exported buffers and the copy constructor keep :meth:`realloc` from moving
168+
the allocation while they use it, but :meth:`tobytes`, :attr:`nbytes`,
169+
:attr:`_pointer`, ``len()`` and ``repr()`` read it without doing so. Under
170+
the GIL none of these can overlap a resize. On a free-threaded build, when
171+
another thread can reach the object during a resize (see :meth:`realloc`),
172+
they may read freed memory, or a pointer and a size that do not belong
173+
together; as with :meth:`realloc`, arranging exclusive access is the
174+
caller's responsibility.
175+
176+
Pickling preserves the content, the alignment, the subclass and the
177+
instance ``__dict__``, but not attributes a subclass stores in
178+
``__slots__``.
166179
"""
167180
cdef void *_memory_ptr
168181
cdef Py_ssize_t _nbytes
@@ -390,6 +403,9 @@ cdef class MKLMemory:
390403
def tobytes(self):
391404
"""
392405
Constructs bytes object populated with copy of this allocation.
406+
407+
The copy does not hold the allocation in place, so it must not overlap
408+
a :meth:`realloc` from another thread.
393409
"""
394410
cdef char* data_ptr = <char*>self._memory_ptr
395411
return data_ptr[:self._nbytes]
@@ -433,4 +449,5 @@ cdef class MKLMemory:
433449
else:
434450
args = (self.tobytes(), self._alignment, cls)
435451

452+
# attributes held in __slots__ of a subclass are not carried over
436453
return (_mkl_memory_from_bytes, args, getattr(self, "__dict__", None))

‎mkl/tests/test_mkl_memory.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,12 +366,41 @@ def test_realloc_preserves_alignment(alignment):
366366

367367
def test_realloc_refcheck_shared():
368368
mem = mkl.MKLMemory(1024)
369+
# enough references that no Python version can call this unique
370+
aliases = [mem] * 3 # noqa: F841
371+
with pytest.raises(ValueError, match="referenced by other objects"):
372+
mem.realloc(2048)
373+
assert mem.nbytes == 1024
374+
del aliases
375+
376+
377+
@pytest.mark.skipif(
378+
sys.version_info < (3, 14),
379+
reason="needs PyUnstable_Object_IsUniquelyReferenced",
380+
)
381+
def test_realloc_refcheck_may_be_shared():
382+
mem = mkl.MKLMemory(1024)
383+
# `mem` is loaded as a borrowed reference, so the refcount during the call
384+
# is exactly 2, which is ambiguous
369385
alias = mem # noqa: F841
370-
with pytest.raises(ValueError, match="referenced by"):
386+
with pytest.raises(ValueError, match="may be referenced by another object"):
371387
mem.realloc(2048)
388+
assert mem.nbytes == 1024
372389
del alias
373390

374391

392+
@pytest.mark.skipif(
393+
sys.version_info < (3, 14),
394+
reason="needs PyUnstable_Object_IsUniquelyReferenced",
395+
)
396+
def test_realloc_uniquely_referenced():
397+
# a bare local is loaded as a borrowed reference, so during the call it is
398+
# uniquely referenced and the resize is allowed
399+
mem = mkl.MKLMemory(1024)
400+
mem.realloc(2048)
401+
assert mem.nbytes == 2048
402+
403+
375404
def test_realloc_refcheck_false_allows_shared():
376405
mem = mkl.MKLMemory(1024)
377406
mv = memoryview(mem)

0 commit comments

Comments
 (0)