diff options
-rw-r--r-- | numpy/core/include/numpy/ufuncobject.h | 8 | ||||
-rw-r--r-- | numpy/core/src/umath/ufunc_object.c | 48 | ||||
-rw-r--r-- | numpy/core/src/umath/umathmodule.c | 1 | ||||
-rw-r--r-- | numpy/lib/tests/test_function_base.py | 49 |
4 files changed, 94 insertions, 12 deletions
diff --git a/numpy/core/include/numpy/ufuncobject.h b/numpy/core/include/numpy/ufuncobject.h index 90d837a9b..15dcdf010 100644 --- a/numpy/core/include/numpy/ufuncobject.h +++ b/numpy/core/include/numpy/ufuncobject.h @@ -120,7 +120,11 @@ typedef struct _tagPyUFuncObject { */ int nin, nout, nargs; - /* Identity for reduction, either PyUFunc_One or PyUFunc_Zero */ + /* + * Identity for reduction, any of PyUFunc_One, PyUFunc_Zero + * PyUFunc_MinusOne, PyUFunc_None, PyUFunc_ReorderableNone, + * PyUFunc_IdentityValue. + */ int identity; /* Array of one-dimensional core loops */ @@ -301,7 +305,7 @@ typedef struct _tagPyUFuncObject { */ #define PyUFunc_ReorderableNone -2 /* - * UFunc unit is in identity_value, and the order of operations can be reordered + * UFunc unit is an identity_value, and the order of operations can be reordered * This case allows reduction with multiple axes at once. */ #define PyUFunc_IdentityValue -3 diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c index ac641c610..ab986caa1 100644 --- a/numpy/core/src/umath/ufunc_object.c +++ b/numpy/core/src/umath/ufunc_object.c @@ -4928,12 +4928,15 @@ PyUFunc_FromFuncAndDataAndSignatureAndIdentity(PyUFuncGenericFunction *func, voi return NULL; } - ufunc = PyArray_malloc(sizeof(PyUFuncObject)); + ufunc = PyObject_GC_New(PyUFuncObject, &PyUFunc_Type); + /* + * We use GC_New here for ufunc->obj, but do not use GC_Track since + * ufunc->obj is still NULL at the end of this function. + * See ufunc_frompyfunc where ufunc->obj is set and GC_Track is called. + */ if (ufunc == NULL) { return NULL; } - memset(ufunc, 0, sizeof(PyUFuncObject)); - PyObject_Init((PyObject *)ufunc, &PyUFunc_Type); ufunc->nin = nin; ufunc->nout = nout; @@ -4941,13 +4944,30 @@ PyUFunc_FromFuncAndDataAndSignatureAndIdentity(PyUFuncGenericFunction *func, voi ufunc->identity = identity; if (ufunc->identity == PyUFunc_IdentityValue) { Py_INCREF(identity_value); + ufunc->identity_value = identity_value; + } + else { + ufunc->identity_value = NULL; } - ufunc->identity_value = identity_value; ufunc->functions = func; ufunc->data = data; ufunc->types = types; ufunc->ntypes = ntypes; + ufunc->core_signature = NULL; + ufunc->core_enabled = 0; + ufunc->obj = NULL; + ufunc->core_num_dims = NULL; + ufunc->core_num_dim_ix = 0; + ufunc->core_offsets = NULL; + ufunc->core_dim_ixs = NULL; + ufunc->core_dim_sizes = NULL; + ufunc->core_dim_flags = NULL; + ufunc->userloops = NULL; + ufunc->ptr = NULL; + ufunc->reserved2 = NULL; + ufunc->reserved1 = 0; + ufunc->iter_flags = 0; /* Type resolution and inner loop selection functions */ ufunc->type_resolver = &PyUFunc_DefaultTypeResolver; @@ -5313,6 +5333,7 @@ PyUFunc_RegisterLoopForType(PyUFuncObject *ufunc, static void ufunc_dealloc(PyUFuncObject *ufunc) { + PyObject_GC_UnTrack((PyObject *)ufunc); PyArray_free(ufunc->core_num_dims); PyArray_free(ufunc->core_dim_ixs); PyArray_free(ufunc->core_dim_sizes); @@ -5322,11 +5343,13 @@ ufunc_dealloc(PyUFuncObject *ufunc) PyArray_free(ufunc->ptr); PyArray_free(ufunc->op_flags); Py_XDECREF(ufunc->userloops); - Py_XDECREF(ufunc->obj); if (ufunc->identity == PyUFunc_IdentityValue) { Py_DECREF(ufunc->identity_value); } - PyArray_free(ufunc); + if (ufunc->obj != NULL) { + Py_DECREF(ufunc->obj); + } + PyObject_GC_Del(ufunc); } static PyObject * @@ -5335,6 +5358,15 @@ ufunc_repr(PyUFuncObject *ufunc) return PyUString_FromFormat("<ufunc '%s'>", ufunc->name); } +static int +ufunc_traverse(PyUFuncObject *self, visitproc visit, void *arg) +{ + Py_VISIT(self->obj); + if (self->identity == PyUFunc_IdentityValue) { + Py_VISIT(self->identity_value); + } + return 0; +} /****************************************************************************** *** UFUNC METHODS *** @@ -6051,9 +6083,9 @@ NPY_NO_EXPORT PyTypeObject PyUFunc_Type = { 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ 0, /* tp_doc */ - 0, /* tp_traverse */ + (traverseproc)ufunc_traverse, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ diff --git a/numpy/core/src/umath/umathmodule.c b/numpy/core/src/umath/umathmodule.c index 4d3151328..23e3ffcfc 100644 --- a/numpy/core/src/umath/umathmodule.c +++ b/numpy/core/src/umath/umathmodule.c @@ -161,6 +161,7 @@ ufunc_frompyfunc(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *NPY_UNUS self->type_resolver = &object_ufunc_type_resolver; self->legacy_inner_loop_selector = &object_ufunc_loop_selector; + PyObject_GC_Track(self); return (PyObject *)self; } diff --git a/numpy/lib/tests/test_function_base.py b/numpy/lib/tests/test_function_base.py index 0976be114..d9a97db1b 100644 --- a/numpy/lib/tests/test_function_base.py +++ b/numpy/lib/tests/test_function_base.py @@ -4,6 +4,7 @@ import operator import warnings import sys import decimal +import types import pytest import numpy as np @@ -24,6 +25,7 @@ from numpy.lib import ( from numpy.compat import long +PY2 = sys.version_info[0] == 2 def get_mat(n): data = np.arange(n) @@ -353,9 +355,9 @@ class TestAverage(object): assert_equal(type(np.average(a, weights=w)), subclass) def test_upcasting(self): - types = [('i4', 'i4', 'f8'), ('i4', 'f4', 'f8'), ('f4', 'i4', 'f8'), + typs = [('i4', 'i4', 'f8'), ('i4', 'f4', 'f8'), ('f4', 'i4', 'f8'), ('f4', 'f4', 'f4'), ('f4', 'f8', 'f8')] - for at, wt, rt in types: + for at, wt, rt in typs: a = np.array([[1,2],[3,4]], dtype=at) w = np.array([[1,2],[3,4]], dtype=wt) assert_equal(np.average(a, weights=w).dtype, np.dtype(rt)) @@ -1498,6 +1500,49 @@ class TestVectorize(object): f(x) +class TestLeaks(object): + class A(object): + iters = 20 + + def bound(self, *args): + return 0 + + @staticmethod + def unbound(*args): + return 0 + + @pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts") + @pytest.mark.parametrize('name, incr', [ + ('bound', A.iters), + ('unbound', 0), + ]) + def test_frompyfunc_leaks(self, name, incr): + # exposed in gh-11867 as np.vectorized, but the problem stems from + # frompyfunc. + # class.attribute = np.frompyfunc(<method>) creates a + # reference cycle if <method> is a bound class method. It requires a + # gc collection cycle to break the cycle (on CPython 3) + import gc + A_func = getattr(self.A, name) + gc.disable() + try: + refcount = sys.getrefcount(A_func) + for i in range(self.A.iters): + a = self.A() + a.f = np.frompyfunc(getattr(a, name), 1, 1) + out = a.f(np.arange(10)) + a = None + if PY2: + assert_equal(sys.getrefcount(A_func), refcount) + else: + # A.func is part of a reference cycle if incr is non-zero + assert_equal(sys.getrefcount(A_func), refcount + incr) + for i in range(5): + gc.collect() + assert_equal(sys.getrefcount(A_func), refcount) + finally: + gc.enable() + class TestDigitize(object): def test_forward(self): |