Skip to content

Commit eba60dc

Browse files
authored
ENH: Reduce compute time for tobytes in non-contiguos paths (numpy#30170)
* Add optimal copy path for non-contiguos arrays in ToString C Impl. * Add tests for obytes new path * Add imports * fix comments * fix memory issues and add benchmarks * run ruff --fix * simplify function * minor fix * minor typos and move tests
1 parent bf56676 commit eba60dc

File tree

3 files changed

+67
-32
lines changed

3 files changed

+67
-32
lines changed

benchmarks/benchmarks/bench_core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def setup(self):
1414
self.l_view = [memoryview(a) for a in self.l]
1515
self.l10x10 = np.ones((10, 10))
1616
self.float64_dtype = np.dtype(np.float64)
17+
self.arr = np.arange(10000).reshape(100, 100)
1718

1819
def time_array_1(self):
1920
np.array(1)
@@ -48,6 +49,9 @@ def time_array_l_view(self):
4849
def time_can_cast(self):
4950
np.can_cast(self.l10x10, self.float64_dtype)
5051

52+
def time_tobytes_noncontiguous(self):
53+
self.arr.T.tobytes()
54+
5155
def time_can_cast_same_kind(self):
5256
np.can_cast(self.l10x10, self.float64_dtype, casting="same_kind")
5357

numpy/_core/src/multiarray/convert.c

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,7 @@ NPY_NO_EXPORT PyObject *
335335
PyArray_ToString(PyArrayObject *self, NPY_ORDER order)
336336
{
337337
npy_intp numbytes;
338-
npy_intp i;
339-
char *dptr;
340-
int elsize;
341338
PyObject *ret;
342-
PyArrayIterObject *it;
343339

344340
if (order == NPY_ANYORDER)
345341
order = PyArray_ISFORTRAN(self) ? NPY_FORTRANORDER : NPY_CORDER;
@@ -354,41 +350,65 @@ PyArray_ToString(PyArrayObject *self, NPY_ORDER order)
354350
numbytes = PyArray_NBYTES(self);
355351
if ((PyArray_IS_C_CONTIGUOUS(self) && (order == NPY_CORDER))
356352
|| (PyArray_IS_F_CONTIGUOUS(self) && (order == NPY_FORTRANORDER))) {
357-
ret = PyBytes_FromStringAndSize(PyArray_DATA(self), (Py_ssize_t) numbytes);
353+
return PyBytes_FromStringAndSize(PyArray_DATA(self), (Py_ssize_t) numbytes);
358354
}
359-
else {
360-
PyObject *new;
361-
if (order == NPY_FORTRANORDER) {
362-
/* iterators are always in C-order */
363-
new = PyArray_Transpose(self, NULL);
364-
if (new == NULL) {
365-
return NULL;
366-
}
355+
356+
/* Avoid Ravel where possible for fewer copies. */
357+
if (!PyDataType_REFCHK(PyArray_DESCR(self)) &&
358+
((PyArray_DESCR(self)->flags & NPY_NEEDS_INIT) == 0)) {
359+
360+
/* Allocate final Bytes Object */
361+
ret = PyBytes_FromStringAndSize(NULL, (Py_ssize_t) numbytes);
362+
if (ret == NULL) {
363+
return NULL;
367364
}
368-
else {
369-
Py_INCREF(self);
370-
new = (PyObject *)self;
365+
366+
/* Writable Buffer */
367+
char* dest = PyBytes_AS_STRING(ret);
368+
369+
int flags = NPY_ARRAY_WRITEABLE;
370+
if (order == NPY_FORTRANORDER) {
371+
flags |= NPY_ARRAY_F_CONTIGUOUS;
371372
}
372-
it = (PyArrayIterObject *)PyArray_IterNew(new);
373-
Py_DECREF(new);
374-
if (it == NULL) {
373+
374+
Py_INCREF(PyArray_DESCR(self));
375+
/* Array view */
376+
PyArrayObject *dest_array = (PyArrayObject *)PyArray_NewFromDescr(
377+
&PyArray_Type,
378+
PyArray_DESCR(self),
379+
PyArray_NDIM(self),
380+
PyArray_DIMS(self),
381+
NULL, // strides
382+
dest,
383+
flags,
384+
NULL
385+
);
386+
387+
if (dest_array == NULL) {
388+
Py_DECREF(ret);
375389
return NULL;
376390
}
377-
ret = PyBytes_FromStringAndSize(NULL, (Py_ssize_t) numbytes);
378-
if (ret == NULL) {
379-
Py_DECREF(it);
391+
392+
/* Copy directly from source to destination with proper ordering */
393+
if (PyArray_CopyInto(dest_array, self) < 0) {
394+
Py_DECREF(dest_array);
395+
Py_DECREF(ret);
380396
return NULL;
381397
}
382-
dptr = PyBytes_AS_STRING(ret);
383-
i = it->size;
384-
elsize = PyArray_ITEMSIZE(self);
385-
while (i--) {
386-
memcpy(dptr, it->dataptr, elsize);
387-
dptr += elsize;
388-
PyArray_ITER_NEXT(it);
389-
}
390-
Py_DECREF(it);
398+
399+
Py_DECREF(dest_array);
400+
return ret;
401+
402+
}
403+
404+
/* Non-contiguous, Has References and/or Init Path. */
405+
PyArrayObject *contig = (PyArrayObject *)PyArray_Ravel(self, order);
406+
if (contig == NULL) {
407+
return NULL;
391408
}
409+
410+
ret = PyBytes_FromStringAndSize(PyArray_DATA(contig), numbytes);
411+
Py_DECREF(contig);
392412
return ret;
393413
}
394414

numpy/_core/tests/test_multiarray.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3835,6 +3835,18 @@ class ArraySubclass(np.ndarray):
38353835
assert_(isinstance(a.ravel('A'), ArraySubclass))
38363836
assert_(isinstance(a.ravel('K'), ArraySubclass))
38373837

3838+
@pytest.mark.parametrize("shape", [(3, 224, 224), (8, 512, 512)])
3839+
def test_tobytes_no_copy_fastpath(self, shape):
3840+
# Test correctness of non-contiguous paths for `tobytes`
3841+
rng = np.random.default_rng(0)
3842+
arr = rng.standard_normal(shape, dtype=np.float32)
3843+
noncontig = arr.transpose(1, 2, 0)
3844+
3845+
# correctness
3846+
expected = np.ascontiguousarray(noncontig).tobytes()
3847+
got = noncontig.tobytes()
3848+
assert got == expected
3849+
38383850
def test_swapaxes(self):
38393851
a = np.arange(1 * 2 * 3 * 4).reshape(1, 2, 3, 4).copy()
38403852
idx = np.indices(a.shape)
@@ -10546,7 +10558,6 @@ def test_getfield():
1054610558
pytest.raises(ValueError, a.getfield, 'uint8', 16)
1054710559
pytest.raises(ValueError, a.getfield, 'uint64', 0)
1054810560

10549-
1055010561
class TestViewDtype:
1055110562
"""
1055210563
Verify that making a view of a non-contiguous array works as expected.

0 commit comments

Comments
 (0)