Repository navigation
ENH: Fast path in ufuncs for exact arrays - #32499
eendebakpt wants to merge 19 commits into
Conversation
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Makes the padding bytes of longdouble scalars deterministic. Co-Authored-By: Claude Fable 5.1 <[email protected]>
| if (ndim == 0) { | ||
| /* 0-d returns a scalar: compute on the stack, skip the temp array. */ | ||
| unary_scalar_storage storage; | ||
| memset(&storage, 0, sizeof(storage)); /* deterministic padding bytes */ |
There was a problem hiding this comment.
Not sure this is needed. For longdouble 10 bytes are used, but due to padding 16 are allocated. For all computations setting the storage to zero is not needed, but if afterwards the results is inspected with tobytes() the padding bytes are visible.
mhvk
left a comment
There was a problem hiding this comment.
@eendebakpt - nice improvements! Two general comments:
- It would be nice to combine the numpy scalar and array scalar cases, as they are super similar. I worry that duplicated code will eventually lead to divergence (bug fixes in only one place, etc.).
- For my work on numpy scalars, I just used the knowledge that
clongdoublewas the longest possible scalar, which is guaranteed to be aligned properly for anything else. That would avoid the whole union and getting a scalar type out.
I'd prefer avoiding that because it relies on platform-dependent behaviors. I asked for the union over in #32269. |
It certainly doesn't hurt to be safe. But maybe then another argument to if not combine, at least make the two fast paths consistent with each other. |
There was a problem hiding this comment.
I asked an AI to review this and it identified some issues, see below.
It also generated the following patch, which saves about 60 lines from the diff and fixes the issues I pointed out
Details
diff --git a/numpy/_core/src/umath/legacy_array_method.c b/numpy/_core/src/umath/legacy_array_method.c
index 36cf9990c0..9769a0362e 100644
--- a/numpy/_core/src/umath/legacy_array_method.c
+++ b/numpy/_core/src/umath/legacy_array_method.c
@@ -71,7 +71,7 @@ wrapped_legacy_resolve_descriptors(PyArrayMethodObject *NPY_UNUSED(self),
* Much the same as the default type resolver, but tries a bit harder to
* preserve metadata.
*/
-static NPY_CASTING
+NPY_NO_EXPORT NPY_CASTING
simple_legacy_resolve_descriptors(
PyArrayMethodObject *method,
PyArray_DTypeMeta *const *dtypes,
diff --git a/numpy/_core/src/umath/legacy_array_method.h b/numpy/_core/src/umath/legacy_array_method.h
index 82eeb04a0a..d48dc2abb1 100644
--- a/numpy/_core/src/umath/legacy_array_method.h
+++ b/numpy/_core/src/umath/legacy_array_method.h
@@ -30,6 +30,12 @@ NPY_NO_EXPORT NPY_CASTING
wrapped_legacy_resolve_descriptors(PyArrayMethodObject *,
PyArray_DTypeMeta *const *, PyArray_Descr *const *, PyArray_Descr **, npy_intp *);
+NPY_NO_EXPORT NPY_CASTING
+simple_legacy_resolve_descriptors(
+ PyArrayMethodObject *method, PyArray_DTypeMeta *const *dtypes,
+ PyArray_Descr *const *given_descrs, PyArray_Descr **output_descrs,
+ npy_intp *view_offset);
+
#ifdef __cplusplus
}
#endif
diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c
index 834ba4cde6..8ea893e1e0 100644
--- a/numpy/_core/src/umath/ufunc_object.c
+++ b/numpy/_core/src/umath/ufunc_object.c
@@ -4731,6 +4731,49 @@ replace_with_wrapped_result_and_return(PyUFuncObject *ufunc,
return NULL;
}
+/*
+ * Run a resolved unary loop, including floating point error handling.
+ * Returns 0 on success and -1 with an error set.
+ */
+static inline int
+execute_unary_trivial_loop(
+ PyUFuncObject *ufunc, PyArrayMethod_Context *context,
+ char **data, npy_intp *strides, npy_intp count)
+{
+ NPY_BEGIN_THREADS_DEF;
+
+ PyArrayMethod_StridedLoop *strided_loop;
+ NpyAuxData *auxdata = NULL;
+ NPY_ARRAYMETHOD_FLAGS flags = 0;
+ if (context->method->get_strided_loop(context, 1, 0, strides,
+ &strided_loop, &auxdata, &flags) < 0) {
+ return -1;
+ }
+
+ if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
+ npy_clear_floatstatus();
+ }
+ if (!(flags & NPY_METH_REQUIRES_PYAPI)) {
+ NPY_BEGIN_THREADS_THRESHOLDED(count);
+ }
+ int ret = strided_loop(context, data, &count, strides, auxdata);
+ NPY_END_THREADS;
+ NPY_AUXDATA_FREE(auxdata);
+
+ if (ret == 0 && PyErr_Occurred()) {
+ ret = -1;
+ }
+ if (ret == 0 && !(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
+ int fpe_errors = npy_get_floatstatus();
+ if (fpe_errors && PyUFunc_GiveFloatingpointErrors(
+ ufunc_get_name_cstr(ufunc), fpe_errors) < 0) {
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+
/*
* Check whether the input object is a known scalar and whether the ufunc has
* a suitable inner loop for it, which takes and returns the data type of the
@@ -4821,43 +4864,11 @@ try_trivial_scalar_call(
context.caller = (PyObject *)ufunc;
context.method = method;
npy_intp strides[2] = {0, 0}; // 0 ensures scalar math, not SIMD for half.
- PyArrayMethod_StridedLoop *strided_loop;
- NpyAuxData *auxdata = NULL;
- NPY_ARRAYMETHOD_FLAGS flags = 0;
- if (method->get_strided_loop(&context, 1, 0, strides,
- &strided_loop, &auxdata, &flags) < 0) {
- ret = -1; // Should not happen, so raise error if it does anyway.
- goto bail;
- }
- /*
- * Call loop with single element, checking floating point errors.
- */
- if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
- npy_clear_floatstatus();
- }
- npy_intp n = 1;
- ret = strided_loop(&context, data, &n, strides, auxdata);
- NPY_AUXDATA_FREE(auxdata);
+ ret = execute_unary_trivial_loop(ufunc, &context, data, strides, 1);
if (ret == 0) {
- if (PyErr_Occurred()) {
- ret = -1;
- goto bail;
- }
- if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
- // Check for any unmasked floating point errors (note: faster
- // than _check_ufunc_fperr as one doesn't need mask up front).
- int fpe_errors = npy_get_floatstatus();
- if (fpe_errors) {
- if (PyUFunc_GiveFloatingpointErrors(
- ufunc_get_name_cstr(ufunc), fpe_errors) < 0) {
- ret = -1; // Real error, falling back would not help.
- goto bail;
- }
- }
- }
*result = PyArray_Scalar(out, dt, NULL);
if (*result == NULL) {
- ret = -1; // Real error (should never happen).
+ ret = -1;
}
}
bail:
@@ -4891,77 +4902,6 @@ typedef union {
npy_clongdouble clongdouble_;
} unary_scalar_storage;
-/* Pointer to the union member for `type_num`, or NULL if unsupported. */
-static inline char *
-unary_scalar_storage_ptr(unary_scalar_storage *storage, int type_num)
-{
- switch (type_num) {
- case NPY_BOOL: return (char *)&storage->bool_;
- case NPY_BYTE: return (char *)&storage->byte_;
- case NPY_UBYTE: return (char *)&storage->ubyte_;
- case NPY_SHORT: return (char *)&storage->short_;
- case NPY_USHORT: return (char *)&storage->ushort_;
- case NPY_INT: return (char *)&storage->int_;
- case NPY_UINT: return (char *)&storage->uint_;
- case NPY_LONG: return (char *)&storage->long_;
- case NPY_ULONG: return (char *)&storage->ulong_;
- case NPY_LONGLONG: return (char *)&storage->longlong_;
- case NPY_ULONGLONG: return (char *)&storage->ulonglong_;
- case NPY_HALF: return (char *)&storage->half_;
- case NPY_FLOAT: return (char *)&storage->float_;
- case NPY_DOUBLE: return (char *)&storage->double_;
- case NPY_LONGDOUBLE: return (char *)&storage->longdouble_;
- case NPY_CFLOAT: return (char *)&storage->cfloat_;
- case NPY_CDOUBLE: return (char *)&storage->cdouble_;
- case NPY_CLONGDOUBLE: return (char *)&storage->clongdouble_;
- default: return NULL;
- }
-}
-
-
-/*
- * Run a resolved unary loop, including floating point error handling.
- * Returns 0 on success and -1 with an error set.
- */
-static inline int
-execute_unary_trivial_loop(
- PyUFuncObject *ufunc, PyArrayMethod_Context *context,
- char **data, npy_intp *strides, npy_intp count)
-{
- NPY_BEGIN_THREADS_DEF;
-
- PyArrayMethod_StridedLoop *strided_loop;
- NpyAuxData *auxdata = NULL;
- NPY_ARRAYMETHOD_FLAGS flags = 0;
- if (context->method->get_strided_loop(context, 1, 0, strides,
- &strided_loop, &auxdata, &flags) < 0) {
- return -1;
- }
-
- if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
- npy_clear_floatstatus();
- }
- if (!(flags & NPY_METH_REQUIRES_PYAPI)) {
- NPY_BEGIN_THREADS_THRESHOLDED(count);
- }
- int ret = strided_loop(context, data, &count, strides, auxdata);
- NPY_END_THREADS;
- NPY_AUXDATA_FREE(auxdata);
-
- if (ret == 0 && PyErr_Occurred()) {
- ret = -1;
- }
- if (ret == 0 && !(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
- int fpe_errors = npy_get_floatstatus();
- if (fpe_errors && PyUFunc_GiveFloatingpointErrors(
- ufunc_get_name_cstr(ufunc), fpe_errors) < 0) {
- ret = -1;
- }
- }
- return ret;
-}
-
-
/*
* Fast path for `ufunc(arr)` with an exact, trivially iterable ndarray
* (0-d, 1-d, or C/F-contiguous) of a builtin non-parametric dtype.
@@ -4980,7 +4920,8 @@ try_unary_trivial_call(
PyArray_Descr *in_descr = PyArray_DESCR(in);
PyArray_DTypeMeta *in_DType = NPY_DTYPE(in_descr);
- if (in_descr != in_DType->singleton || !PyArray_ISALIGNED(in)) {
+ if (!PyDataType_ISNUMBER(in_descr)
+ || in_descr != in_DType->singleton || !PyArray_ISALIGNED(in)) {
return -2;
}
int ndim = PyArray_NDIM(in);
@@ -5007,7 +4948,8 @@ try_unary_trivial_call(
PyArrayMethodObject *method =
(PyArrayMethodObject *)PyTuple_GET_ITEM(info, 1);
if (!PyObject_TypeCheck(method, &PyArrayMethod_Type)
- || method->resolve_descriptors_with_scalars != NULL) {
+ || method->resolve_descriptors_with_scalars != NULL
+ || method->resolve_descriptors != &simple_legacy_resolve_descriptors) {
return -2;
}
PyObject *all_dtypes = PyTuple_GET_ITEM(info, 0);
@@ -5018,12 +4960,8 @@ try_unary_trivial_call(
PyArray_DTypeMeta *out_DType =
(PyArray_DTypeMeta *)PyTuple_GET_ITEM(all_dtypes, 1);
PyArray_Descr *out_descr = out_DType->singleton;
- /* No parametric (datetime, strings) or refcounted dtypes, and no
- * finalize_descr (the allocation could replace the descr). */
- if (out_descr == NULL || PyDataType_REFCHK(in_descr)
- || PyDataType_REFCHK(out_descr)
- || NPY_DT_is_parametric(in_DType) || NPY_DT_is_parametric(out_DType)
- || NPY_DT_has_finalize(out_DType)) {
+ /* Only builtin numeric/bool descriptors can bypass resolution. */
+ if (out_descr == NULL || !PyDataType_ISNUMBER(out_descr)) {
return -2;
}
@@ -5037,10 +4975,7 @@ try_unary_trivial_call(
/* 0-d returns a scalar: compute on the stack, skip the temp array. */
unary_scalar_storage storage;
memset(&storage, 0, sizeof(storage)); /* deterministic padding bytes */
- char *out_buf = unary_scalar_storage_ptr(&storage, out_descr->type_num);
- if (out_buf == NULL) {
- return -2;
- }
+ char *out_buf = (char *)&storage;
char *data[2] = {PyArray_BYTES(in), out_buf};
npy_intp strides[2] = {0, 0}; // 0 ensures scalar math, not SIMD
if (execute_unary_trivial_loop(ufunc, &context, data, strides, 1) < 0) {
@@ -5050,10 +4985,9 @@ try_unary_trivial_call(
return *result == NULL ? -1 : 0;
}
- PyArrayObject *out = (PyArrayObject *)PyArray_NewFromDescr_int(
+ PyArrayObject *out = (PyArrayObject *)PyArray_NewFromDescr(
&PyArray_Type, (PyArray_Descr *)Py_NewRef(out_descr),
- ndim, PyArray_SHAPE(in), NULL, NULL, out_fortran,
- NULL, NULL, _NPY_ARRAY_ENSURE_DTYPE_IDENTITY);
+ ndim, PyArray_SHAPE(in), NULL, NULL, out_fortran, NULL);
if (out == NULL) {
return -1;
}
| if (memcmp(item+1, key, key_len * sizeof(PyObject *)) == 0) { | ||
| int match = 1; | ||
| for (int i = 0; i < key_len; i++) { | ||
| if (item[i + 1] != key[i]) { | ||
| match = 0; | ||
| break; | ||
| } | ||
| } | ||
| if (match) { |
There was a problem hiding this comment.
This change looks unrelated to the other work. It also looks a bit dubious to me, memcmp is pretty fast!
There was a problem hiding this comment.
Seems to be faster. Unrelated, so I will remove it and revisit later.
| PyArrayMethodObject *method = | ||
| (PyArrayMethodObject *)PyTuple_GET_ITEM(info, 1); | ||
| if (!PyObject_TypeCheck(method, &PyArrayMethod_Type) | ||
| || method->resolve_descriptors_with_scalars != NULL) { |
There was a problem hiding this comment.
Consider the following patch on top of your PR, which adds a test ufunc that this check does not sufficiently capture, leading to a caching bug:
Details
diff --git a/numpy/_core/src/umath/_umath_tests.c.src b/numpy/_core/src/umath/_umath_tests.c.src
index ed8058eb8a..b887e10183 100644
--- a/numpy/_core/src/umath/_umath_tests.c.src
+++ b/numpy/_core/src/umath/_umath_tests.c.src
@@ -969,6 +969,62 @@ UMath_Tests_create_unary_ufunc(PyObject *NPY_UNUSED(dummy),
"fresh unary float64 test ufunc (negation)", 0);
}
+static NPY_CASTING
+unary_error_resolve_descriptors(
+ struct PyArrayMethodObject_tag *NPY_UNUSED(method),
+ PyArray_DTypeMeta *const *NPY_UNUSED(dtypes),
+ PyArray_Descr *const *NPY_UNUSED(given_descrs),
+ PyArray_Descr **NPY_UNUSED(loop_descrs),
+ npy_intp *NPY_UNUSED(view_offset))
+{
+ PyErr_SetString(PyExc_TypeError, "unary descriptor resolution failed");
+ return (NPY_CASTING)-1;
+}
+
+static int
+unary_unexpected_loop(PyArrayMethod_Context *NPY_UNUSED(context),
+ char *const *NPY_UNUSED(data),
+ const npy_intp *NPY_UNUSED(dimensions),
+ const npy_intp *NPY_UNUSED(strides), NpyAuxData *NPY_UNUSED(auxdata))
+{
+ PyErr_SetString(PyExc_AssertionError,
+ "unary loop executed without descriptor resolution");
+ return -1;
+}
+
+static PyObject *
+UMath_Tests_create_unary_ufunc_with_resolver_error(
+ PyObject *NPY_UNUSED(self), PyObject *NPY_UNUSED(args))
+{
+ PyObject *ufunc = PyUFunc_FromFuncAndData(
+ NULL, NULL, NULL, 0, 1, 1, PyUFunc_None,
+ "unary_resolver_error", NULL, 0);
+ if (ufunc == NULL) {
+ return NULL;
+ }
+ PyArray_DTypeMeta *dtypes[2] = {
+ &PyArray_DoubleDType, &PyArray_DoubleDType};
+ PyType_Slot slots[] = {
+ {NPY_METH_resolve_descriptors, &unary_error_resolve_descriptors},
+ {NPY_METH_strided_loop, &unary_unexpected_loop},
+ {0, NULL},
+ };
+ PyArrayMethod_Spec spec = {
+ .name = "unary_resolver_error",
+ .nin = 1,
+ .nout = 1,
+ .casting = NPY_NO_CASTING,
+ .flags = NPY_METH_REQUIRES_PYAPI,
+ .dtypes = dtypes,
+ .slots = slots,
+ };
+ if (PyUFunc_AddLoopFromSpec(ufunc, &spec) < 0) {
+ Py_DECREF(ufunc);
+ return NULL;
+ }
+ return ufunc;
+}
+
static PyMethodDef UMath_TestsMethods[] = {
{"test_signature", UMath_Tests_test_signature, METH_VARARGS,
"Test signature parsing of ufunc. \n"
@@ -986,6 +1042,9 @@ static PyMethodDef UMath_TestsMethods[] = {
{"create_unary_ufunc", UMath_Tests_create_unary_ufunc, METH_NOARGS,
"Create a fresh unary float64 ufunc (negation) with untouched "
"dispatch state.\n"},
+ {"create_unary_ufunc_with_resolver_error",
+ UMath_Tests_create_unary_ufunc_with_resolver_error, METH_NOARGS,
+ "Create a fresh unary float64 ufunc whose descriptor resolver raises.\n"},
{NULL, NULL, 0, NULL} /* Sentinel */
};
diff --git a/numpy/_core/tests/test_umath.py b/numpy/_core/tests/test_umath.py
index 1a9fdb6c72..09c985dfc8 100644
--- a/numpy/_core/tests/test_umath.py
+++ b/numpy/_core/tests/test_umath.py
@@ -3261,6 +3261,19 @@ class TestUnaryTrivialCall:
# ``ufunc(arr)`` for an exact, trivially iterable ndarray with a builtin
# dtype takes a fast path (``try_unary_trivial_call``).
+ @pytest.mark.parametrize("shape", [(), (2,)])
+ def test_cached_descriptor_resolver_error(self, shape):
+ ufunc = ncu_tests.create_unary_ufunc_with_resolver_error()
+ arr = np.ones(shape, dtype=np.float64)
+
+ # The first call caches the ArrayMethod before its resolver raises.
+ with pytest.raises(TypeError, match="unary descriptor resolution failed"):
+ ufunc(arr)
+
+ # A cache hit must still invoke the resolver, without running the loop.
+ with pytest.raises(TypeError, match="unary descriptor resolution failed"):
+ ufunc(arr)
+
@pytest.mark.parametrize("dtype", ["?", "i1", "u8", "e", "d", "g", "G"])
def test_0d_returns_scalar(self, dtype):
arr = np.array([1, 0], dtype=dtype)The fix is to also add a check that specifically the legacy resolve_descriptors implementation is running
| if (out_descr == NULL || PyDataType_REFCHK(in_descr) | ||
| || PyDataType_REFCHK(out_descr) | ||
| || NPY_DT_is_parametric(in_DType) || NPY_DT_is_parametric(out_DType) | ||
| || NPY_DT_has_finalize(out_DType)) { |
There was a problem hiding this comment.
Couldn't you replace this with !(PyDataType_ISNUMBER(in_descr) || PyDataType_ISNUMBER(out_descr)? That includes all the types in the union, I think.
| * Returns 0 on success and -1 with an error set. | ||
| */ | ||
| static inline int | ||
| execute_unary_trivial_loop( |
There was a problem hiding this comment.
This should probably share code with the scalar shortcut
| switch (type_num) { | ||
| case NPY_BOOL: return (char *)&storage->bool_; | ||
| case NPY_BYTE: return (char *)&storage->byte_; | ||
| case NPY_UBYTE: return (char *)&storage->ubyte_; | ||
| case NPY_SHORT: return (char *)&storage->short_; | ||
| case NPY_USHORT: return (char *)&storage->ushort_; | ||
| case NPY_INT: return (char *)&storage->int_; | ||
| case NPY_UINT: return (char *)&storage->uint_; | ||
| case NPY_LONG: return (char *)&storage->long_; | ||
| case NPY_ULONG: return (char *)&storage->ulong_; | ||
| case NPY_LONGLONG: return (char *)&storage->longlong_; | ||
| case NPY_ULONGLONG: return (char *)&storage->ulonglong_; | ||
| case NPY_HALF: return (char *)&storage->half_; | ||
| case NPY_FLOAT: return (char *)&storage->float_; | ||
| case NPY_DOUBLE: return (char *)&storage->double_; | ||
| case NPY_LONGDOUBLE: return (char *)&storage->longdouble_; | ||
| case NPY_CFLOAT: return (char *)&storage->cfloat_; | ||
| case NPY_CDOUBLE: return (char *)&storage->cdouble_; | ||
| case NPY_CLONGDOUBLE: return (char *)&storage->clongdouble_; | ||
| default: return NULL; |
There was a problem hiding this comment.
Couldn't this whole function be replaced by (char *)&storage? That doesn't have type punning issues because any pointer type can be cast to char* and you're always just returning the address of the first byte above.
| np.positive(np.array(['bar'], dtype=object)) | ||
|
|
||
|
|
||
| class TestUnaryTrivialCall: |
There was a problem hiding this comment.
Try to think about other possible corner cases around the use of caching in the implementation. I also don't see tests for unusual arrays with negative or zero strides or empty arrays.
A cached ArrayMethod with a custom resolve_descriptors must still have it called; the fast paths (array and scalar) now require the legacy simple_legacy_resolve_descriptors, whose result is the DType singleton. Also, from review: restrict the array fast path to builtin numeric dtypes (PyDataType_ISNUMBER), drop the union member switch, use the public PyArray_NewFromDescr, and share the loop execution with the scalar fast path. Adds a test ufunc whose resolver raises. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The existing scalar fast path does not do so either. Co-Authored-By: Claude Fable 5.1 <[email protected]>
…fast paths The Python-scalar shortcut and the 0-d array fast path now use the same helpers to find the cached trivial loop and to run it into stack storage; the scalar shortcut thereby also handles loops with a different builtin output dtype (e.g. isnan). Co-Authored-By: Claude Fable 5.1 <[email protected]>
…32269 Co-Authored-By: Claude Fable 5.1 <[email protected]>
Shared name with numpygh-32499, which uses the same union. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Fixes the stubtest CI failure. Co-Authored-By: Claude Fable 5.1 <[email protected]>
ngoldbaum
left a comment
There was a problem hiding this comment.
There's a merge conflict but otherwise I think this looks good! I also spotted two minor simplification opportunities.
Because this is a decent-sized chunk of code, I've gone ahead and added the triage review label and the 2.6 milestone. Hopefully that attracts at least one more review pass from another maintainer.
There was a problem hiding this comment.
While we're here and now that we have a nice name for it, I think this is the same as:
cin.cdouble_ = npy_cpack(oop.real, oop.imag)| int ndim = PyArray_NDIM(in); | ||
| npy_intp in_stride = in_descr->elsize; | ||
| int out_fortran = 0; | ||
| if (ndim == 1) { | ||
| in_stride = PyArray_STRIDE(in, 0); | ||
| } | ||
| else if (ndim > 1 && !PyArray_IS_C_CONTIGUOUS(in)) { | ||
| if (!PyArray_IS_F_CONTIGUOUS(in)) { | ||
| /* Multiple memory segments need the full iterator. */ | ||
| return -2; | ||
| } | ||
| out_fortran = 1; | ||
| } |
There was a problem hiding this comment.
| int ndim = PyArray_NDIM(in); | |
| npy_intp in_stride = in_descr->elsize; | |
| int out_fortran = 0; | |
| if (ndim == 1) { | |
| in_stride = PyArray_STRIDE(in, 0); | |
| } | |
| else if (ndim > 1 && !PyArray_IS_C_CONTIGUOUS(in)) { | |
| if (!PyArray_IS_F_CONTIGUOUS(in)) { | |
| /* Multiple memory segments need the full iterator. */ | |
| return -2; | |
| } | |
| out_fortran = 1; | |
| } | |
| int ndim = PyArray_NDIM(in); | |
| if (!PyArray_TRIVIALLY_ITERABLE(in)) { | |
| return -2; | |
| } | |
| npy_intp in_stride = num == 1 ? PyArray_STRIDE(in, 0) : in_descr->elsize; | |
| int out_fortran = PyArray_ISFORTRAN(in) |
I think this is equivalent to what you have above, please double-check
# Conflicts: # benchmarks/benchmarks/bench_ufunc.py
Use npy_cpack to fill the complex scalar input and PyArray_TRIVIALLY_ITERABLE/PyArray_ISFORTRAN for the layout check. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
PR summary
This PR adds a fast path for unary ufunc calls
ufunc(arr)wherearris an exactndarraythat is trivially iterable (0-d, 1-d, or C/F-contiguous) with a builtin non-parametric dtype, e.g.np.sin(x)ornp.abs(x)for a float or integer array.The approach for 0d arrays is similar to #32269.
Fixes #11232
Benchmarks:
np.sin(np.array(1.5))np.sin(np.arange(4.0))np.negative(np.arange(4))np.abs(np.arange(4.0))np.sin(np.arange(100.0))np.add(a4, a4)(binary, not affected)Because the fast path no longer reference counts the input array and its DType class, scaling on free-threaded builds improves as well.
Benchmark script
AI Disclosure
Claude was used in generation and benchmarking of the code.