From ad608b5466b71400ece11f402f90de7bf3568e55 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Mon, 26 May 2025 15:22:30 +0530 Subject: [PATCH 1/5] make PyCFuncPtr_call lock free --- Modules/_ctypes/_ctypes.c | 149 +++++++++++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 35 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 7de6bb396b084e..75c5e1fc9e161d 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3610,6 +3610,44 @@ generic_pycdata_new(ctypes_state *st, PyCFuncPtr_Type */ +static inline void +atomic_xsetref(PyObject **field, PyObject *value) +{ +#ifdef Py_GIL_DISABLED + PyObject *old = *field; + _Py_atomic_store_ptr(field, value); + Py_XDECREF(old); +#else + Py_XSETREF(*field, value); +#endif +} +/* + This function atomically loads the reference from *field, and + tries to get a new reference to it. If the increment fails, + it acquires critical section of obj and returns a new reference to the *field. +*/ +static inline PyObject * +atomic_xgetref(PyObject *obj, PyObject **field) +{ +#ifdef Py_GIL_DISABLED + PyObject *value = _Py_atomic_load_ptr(field); + if (value == NULL) { + return NULL; + } + if (_Py_TryIncrefCompare(field, value)) { + return value; + } + Py_BEGIN_CRITICAL_SECTION(obj); + value = Py_XNewRef(*field); + Py_END_CRITICAL_SECTION(); + return value; +#else + return Py_XNewRef(*field); +#endif +} + + + /*[clinic input] @critical_section @setter @@ -3626,7 +3664,7 @@ _ctypes_CFuncPtr_errcheck_set_impl(PyCFuncPtrObject *self, PyObject *value) return -1; } Py_XINCREF(value); - Py_XSETREF(self->errcheck, value); + atomic_xsetref(&self->errcheck, value); return 0; } @@ -3658,12 +3696,10 @@ static int _ctypes_CFuncPtr_restype_set_impl(PyCFuncPtrObject *self, PyObject *value) /*[clinic end generated code: output=0be0a086abbabf18 input=683c3bef4562ccc6]*/ { - PyObject *checker, *oldchecker; + PyObject *checker; if (value == NULL) { - oldchecker = self->checker; - self->checker = NULL; - Py_CLEAR(self->restype); - Py_XDECREF(oldchecker); + atomic_xsetref(&self->restype, NULL); + atomic_xsetref(&self->checker, NULL); return 0; } ctypes_state *st = get_module_state_by_def(Py_TYPE(Py_TYPE(self))); @@ -3679,11 +3715,9 @@ _ctypes_CFuncPtr_restype_set_impl(PyCFuncPtrObject *self, PyObject *value) if (PyObject_GetOptionalAttr(value, &_Py_ID(_check_retval_), &checker) < 0) { return -1; } - oldchecker = self->checker; - self->checker = checker; Py_INCREF(value); - Py_XSETREF(self->restype, value); - Py_XDECREF(oldchecker); + atomic_xsetref(&self->checker, checker); + atomic_xsetref(&self->restype, value); return 0; } @@ -3728,16 +3762,16 @@ _ctypes_CFuncPtr_argtypes_set_impl(PyCFuncPtrObject *self, PyObject *value) PyObject *converters; if (value == NULL || value == Py_None) { - Py_CLEAR(self->converters); - Py_CLEAR(self->argtypes); + atomic_xsetref(&self->argtypes, NULL); + atomic_xsetref(&self->converters, NULL); } else { ctypes_state *st = get_module_state_by_def(Py_TYPE(Py_TYPE(self))); converters = converters_from_argtypes(st, value); if (!converters) return -1; - Py_XSETREF(self->converters, converters); + atomic_xsetref(&self->converters, converters); Py_INCREF(value); - Py_XSETREF(self->argtypes, value); + atomic_xsetref(&self->argtypes, value); } return 0; } @@ -4533,9 +4567,8 @@ _build_result(PyObject *result, PyObject *callargs, } static PyObject * -PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) +PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); PyObject *restype; PyObject *converters; PyObject *checker; @@ -4560,13 +4593,24 @@ PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) } assert(info); /* Cannot be NULL for PyCFuncPtrObject instances */ - restype = self->restype ? self->restype : info->restype; - converters = self->converters ? self->converters : info->converters; - checker = self->checker ? self->checker : info->checker; - argtypes = self->argtypes ? self->argtypes : info->argtypes; -/* later, we probably want to have an errcheck field in stginfo */ - errcheck = self->errcheck /* ? self->errcheck : info->errcheck */; - + restype = atomic_xgetref(op, &self->restype); + if (restype == NULL) { + restype = Py_XNewRef(info->restype); + } + converters = atomic_xgetref(op, &self->converters); + if (converters == NULL) { + converters = Py_XNewRef(info->converters); + } + checker = atomic_xgetref(op, &self->checker); + if (checker == NULL) { + checker = Py_XNewRef(info->checker); + } + argtypes = atomic_xgetref(op, &self->argtypes); + if (argtypes == NULL) { + argtypes = Py_XNewRef(info->argtypes); + } + /* later, we probably want to have an errcheck field in stginfo */ + errcheck = atomic_xgetref(op, &self->errcheck); pProc = *(void **)self->b_ptr; #ifdef MS_WIN32 @@ -4575,11 +4619,21 @@ PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) CDataObject *this; this = (CDataObject *)PyTuple_GetItem(inargs, 0); /* borrowed ref! */ if (!this) { + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); PyErr_SetString(PyExc_ValueError, "native com method call without 'this' parameter"); return NULL; } if (!CDataObject_Check(st, this)) { + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); PyErr_SetString(PyExc_TypeError, "Expected a COM this pointer as first argument"); return NULL; @@ -4587,12 +4641,22 @@ PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) /* there should be more checks? No, in Python */ /* First arg is a pointer to an interface instance */ if (!this->b_ptr || *(void **)this->b_ptr == NULL) { + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); PyErr_SetString(PyExc_ValueError, "NULL COM pointer access"); return NULL; } piunk = *(IUnknown **)this->b_ptr; if (NULL == piunk->lpVtbl) { + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); PyErr_SetString(PyExc_ValueError, "COM method call without VTable"); return NULL; @@ -4603,8 +4667,14 @@ PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) callargs = _build_callargs(st, self, argtypes, inargs, kwds, &outmask, &inoutmask, &numretvals); - if (callargs == NULL) + if (callargs == NULL) { + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); return NULL; + } if (converters) { int required = Py_SAFE_DOWNCAST(PyTuple_GET_SIZE(converters), @@ -4618,6 +4688,11 @@ PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) */ if (required > actual) { Py_DECREF(callargs); + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); PyErr_Format(PyExc_TypeError, "this function takes at least %d argument%s (%d given)", required, @@ -4627,6 +4702,11 @@ PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) } } else if (required != actual) { Py_DECREF(callargs); + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); PyErr_Format(PyExc_TypeError, "this function takes %d argument%s (%d given)", required, @@ -4663,25 +4743,24 @@ PyCFuncPtr_call_lock_held(PyObject *op, PyObject *inargs, PyObject *kwds) if (v == NULL || v != callargs) { Py_DECREF(result); Py_DECREF(callargs); + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); return v; } Py_DECREF(v); } - + Py_XDECREF(restype); + Py_XDECREF(converters); + Py_XDECREF(checker); + Py_XDECREF(argtypes); + Py_XDECREF(errcheck); return _build_result(result, callargs, outmask, inoutmask, numretvals); } -static PyObject * -PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = PyCFuncPtr_call_lock_held(op, inargs, kwds); - Py_END_CRITICAL_SECTION(); - return result; -} - static int PyCFuncPtr_traverse(PyObject *op, visitproc visit, void *arg) { From 28643b75c5ca94ec7cb13a6af5b3cca81b0448d6 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Mon, 26 May 2025 15:36:49 +0530 Subject: [PATCH 2/5] add comments --- Modules/_ctypes/_ctypes.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 75c5e1fc9e161d..35931620f45937 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3623,8 +3623,9 @@ atomic_xsetref(PyObject **field, PyObject *value) } /* This function atomically loads the reference from *field, and - tries to get a new reference to it. If the increment fails, + tries to get a new reference to it. If the incref fails, it acquires critical section of obj and returns a new reference to the *field. + In the general case, this avoids contention on acquiring the critical section. */ static inline PyObject * atomic_xgetref(PyObject *obj, PyObject **field) From 8b506815040d204710dd0a8993e93173b76c046e Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Mon, 26 May 2025 20:27:24 +0530 Subject: [PATCH 3/5] use goto --- Modules/_ctypes/_ctypes.c | 82 ++++++++++----------------------------- 1 file changed, 20 insertions(+), 62 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 35931620f45937..7e8a133caa72ac 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -4570,13 +4570,9 @@ _build_result(PyObject *result, PyObject *callargs, static PyObject * PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) { - PyObject *restype; - PyObject *converters; - PyObject *checker; - PyObject *argtypes; - PyObject *result; - PyObject *callargs; - PyObject *errcheck; + PyObject *result = NULL; + PyObject *callargs = NULL; + PyObject *ret = NULL; #ifdef MS_WIN32 IUnknown *piunk = NULL; #endif @@ -4594,24 +4590,24 @@ PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) } assert(info); /* Cannot be NULL for PyCFuncPtrObject instances */ - restype = atomic_xgetref(op, &self->restype); + PyObject *restype = atomic_xgetref(op, &self->restype); if (restype == NULL) { restype = Py_XNewRef(info->restype); } - converters = atomic_xgetref(op, &self->converters); + PyObject *converters = atomic_xgetref(op, &self->converters); if (converters == NULL) { converters = Py_XNewRef(info->converters); } - checker = atomic_xgetref(op, &self->checker); + PyObject *checker = atomic_xgetref(op, &self->checker); if (checker == NULL) { checker = Py_XNewRef(info->checker); } - argtypes = atomic_xgetref(op, &self->argtypes); + PyObject *argtypes = atomic_xgetref(op, &self->argtypes); if (argtypes == NULL) { argtypes = Py_XNewRef(info->argtypes); } /* later, we probably want to have an errcheck field in stginfo */ - errcheck = atomic_xgetref(op, &self->errcheck); + PyObject *errcheck = atomic_xgetref(op, &self->errcheck); pProc = *(void **)self->b_ptr; #ifdef MS_WIN32 @@ -4620,47 +4616,27 @@ PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) CDataObject *this; this = (CDataObject *)PyTuple_GetItem(inargs, 0); /* borrowed ref! */ if (!this) { - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); PyErr_SetString(PyExc_ValueError, "native com method call without 'this' parameter"); - return NULL; + goto finally; } if (!CDataObject_Check(st, this)) { - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); PyErr_SetString(PyExc_TypeError, "Expected a COM this pointer as first argument"); - return NULL; + goto finally; } /* there should be more checks? No, in Python */ /* First arg is a pointer to an interface instance */ if (!this->b_ptr || *(void **)this->b_ptr == NULL) { - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); PyErr_SetString(PyExc_ValueError, "NULL COM pointer access"); - return NULL; + goto finally; } piunk = *(IUnknown **)this->b_ptr; if (NULL == piunk->lpVtbl) { - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); PyErr_SetString(PyExc_ValueError, "COM method call without VTable"); - return NULL; + goto finally; } pProc = ((void **)piunk->lpVtbl)[self->index - 0x1000]; } @@ -4669,12 +4645,7 @@ PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) inargs, kwds, &outmask, &inoutmask, &numretvals); if (callargs == NULL) { - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); - return NULL; + goto finally; } if (converters) { @@ -4689,31 +4660,21 @@ PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) */ if (required > actual) { Py_DECREF(callargs); - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); PyErr_Format(PyExc_TypeError, "this function takes at least %d argument%s (%d given)", required, required == 1 ? "" : "s", actual); - return NULL; + goto finally; } } else if (required != actual) { Py_DECREF(callargs); - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); PyErr_Format(PyExc_TypeError, "this function takes %d argument%s (%d given)", required, required == 1 ? "" : "s", actual); - return NULL; + goto finally; } } @@ -4744,22 +4705,19 @@ PyCFuncPtr_call(PyObject *op, PyObject *inargs, PyObject *kwds) if (v == NULL || v != callargs) { Py_DECREF(result); Py_DECREF(callargs); - Py_XDECREF(restype); - Py_XDECREF(converters); - Py_XDECREF(checker); - Py_XDECREF(argtypes); - Py_XDECREF(errcheck); - return v; + ret = v; + goto finally; } Py_DECREF(v); } + ret = _build_result(result, callargs, outmask, inoutmask, numretvals); +finally: Py_XDECREF(restype); Py_XDECREF(converters); Py_XDECREF(checker); Py_XDECREF(argtypes); Py_XDECREF(errcheck); - return _build_result(result, callargs, - outmask, inoutmask, numretvals); + return ret; } static int From 095e1cb48e99b94d3161290ea61d4db4d7cd19da Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 17:06:46 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst b/Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst new file mode 100644 index 00000000000000..e7b8dc21061e43 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst @@ -0,0 +1 @@ +Fix performance regression in calling ``ctypes._CFuncPtr`` in free-threading. From c5309d6755299c8186527f8d113b4aae65a2fb00 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Mon, 26 May 2025 23:18:09 +0530 Subject: [PATCH 5/5] Update Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst Co-authored-by: Peter Bierma --- .../next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst b/Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst index e7b8dc21061e43..2a4d8725210834 100644 --- a/Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst +++ b/Misc/NEWS.d/next/Library/2025-05-26-17-06-40.gh-issue-134637.9-3zRL.rst @@ -1 +1 @@ -Fix performance regression in calling ``ctypes._CFuncPtr`` in free-threading. +Fix performance regression in calling a :mod:`ctypes` function pointer in :term:`free threading`.