diff --git a/Include/internal/pycore_call.h b/Include/internal/pycore_call.h index 32ac3d17f22077..05b58b2ba54a10 100644 --- a/Include/internal/pycore_call.h +++ b/Include/internal/pycore_call.h @@ -98,6 +98,14 @@ _PyObject_CallMethodIdOneArg(PyObject *self, _Py_Identifier *name, PyObject *arg } +extern PyObject *_PyObject_VectorcallPrepend( + PyThreadState *tstate, + PyObject *callable, + PyObject *arg, + PyObject *const *args, + size_t nargsf, + PyObject *kwnames); + /* === Vectorcall protocol (PEP 590) ============================= */ // Call callable using tp_call. Arguments are like PyObject_Vectorcall(), diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index 4e197c2e29ec89..e3fa8e10eb447e 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -383,6 +383,11 @@ extern int _PyRunRemoteDebugger(PyThreadState *tstate); #define SPECIAL___AEXIT__ 3 #define SPECIAL_MAX 3 +PyAPI_FUNC(_PyStackRef) +_Py_LoadAttr_StackRefSteal( + PyThreadState *tstate, _PyStackRef owner, + PyObject *name, _PyStackRef *self_or_null); + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_function.h b/Include/internal/pycore_function.h index 6e1209659565a3..69b9ad548295c7 100644 --- a/Include/internal/pycore_function.h +++ b/Include/internal/pycore_function.h @@ -47,6 +47,17 @@ static inline PyObject* _PyFunction_GET_BUILTINS(PyObject *func) { #define _PyFunction_GET_BUILTINS(func) _PyFunction_GET_BUILTINS(_PyObject_CAST(func)) +/* Get the callable wrapped by a classmethod. + Returns a borrowed reference. + The caller must ensure 'cm' is a classmethod object. */ +extern PyObject *_PyClassMethod_GetFunc(PyObject *cm); + +/* Get the callable wrapped by a staticmethod. + Returns a borrowed reference. + The caller must ensure 'sm' is a staticmethod object. */ +extern PyObject *_PyStaticMethod_GetFunc(PyObject *sm); + + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 12c5614834f565..ca44b2c10998a9 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -897,6 +897,9 @@ extern PyObject *_PyType_LookupRefAndVersion(PyTypeObject *, PyObject *, extern unsigned int _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out); +extern int _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, + PyObject *name, _PyStackRef *method); + // Cache the provided init method in the specialization cache of type if the // provided type version matches the current version of the type. // diff --git a/Include/internal/pycore_stackref.h b/Include/internal/pycore_stackref.h index c977d29e9d3259..42fdbeb21f652a 100644 --- a/Include/internal/pycore_stackref.h +++ b/Include/internal/pycore_stackref.h @@ -127,6 +127,13 @@ _PyStackRef_FromPyObjectSteal(PyObject *obj, const char *filename, int linenumbe } #define PyStackRef_FromPyObjectSteal(obj) _PyStackRef_FromPyObjectSteal(_PyObject_CAST(obj), __FILE__, __LINE__) +static inline _PyStackRef +_PyStackRef_FromPyObjectBorrow(PyObject *obj, const char *filename, int linenumber) +{ + return _Py_stackref_create(obj, filename, linenumber); +} +#define PyStackRef_FromPyObjectBorrow(obj) _PyStackRef_FromPyObjectBorrow(_PyObject_CAST(obj), __FILE__, __LINE__) + static inline _PyStackRef _PyStackRef_FromPyObjectImmortal(PyObject *obj, const char *filename, int linenumber) { @@ -320,6 +327,14 @@ _PyStackRef_FromPyObjectSteal(PyObject *obj) } # define PyStackRef_FromPyObjectSteal(obj) _PyStackRef_FromPyObjectSteal(_PyObject_CAST(obj)) +static inline _PyStackRef +PyStackRef_FromPyObjectBorrow(PyObject *obj) +{ + assert(obj != NULL); + assert(((uintptr_t)obj & Py_TAG_BITS) == 0); + return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_DEFERRED }; +} + static inline bool PyStackRef_IsHeapSafe(_PyStackRef stackref) { @@ -538,6 +553,13 @@ PyStackRef_FromPyObjectSteal(PyObject *obj) return ref; } +static inline _PyStackRef +PyStackRef_FromPyObjectBorrow(PyObject *obj) +{ + assert(obj != NULL); + return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_REFCNT }; +} + static inline _PyStackRef PyStackRef_FromPyObjectStealMortal(PyObject *obj) { @@ -753,6 +775,17 @@ _PyThreadState_PopCStackRef(PyThreadState *tstate, _PyCStackRef *ref) PyStackRef_XCLOSE(ref->ref); } +static inline _PyStackRef +_PyThreadState_PopCStackRefSteal(PyThreadState *tstate, _PyCStackRef *ref) +{ +#ifdef Py_GIL_DISABLED + _PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate; + assert(tstate_impl->c_stack_refs == ref); + tstate_impl->c_stack_refs = ref->next; +#endif + return ref->ref; +} + #ifdef Py_GIL_DISABLED static inline int diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-10-22-38-40.gh-issue-145779.5375381d80.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-10-22-38-40.gh-issue-145779.5375381d80.rst new file mode 100644 index 00000000000000..9cd0263a107f16 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-10-22-38-40.gh-issue-145779.5375381d80.rst @@ -0,0 +1,2 @@ +Improve scaling of :func:`classmethod` and :func:`staticmethod` calls in +the free-threaded build by avoiding the descriptor ``__get__`` call. diff --git a/Objects/call.c b/Objects/call.c index 6e331a43899a06..0299e458b1d879 100644 --- a/Objects/call.c +++ b/Objects/call.c @@ -825,6 +825,60 @@ object_vacall(PyThreadState *tstate, PyObject *base, return result; } +PyObject * +_PyObject_VectorcallPrepend(PyThreadState *tstate, PyObject *callable, + PyObject *arg, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + assert(nargs == 0 || args[nargs-1]); + + PyObject *result; + if (nargsf & PY_VECTORCALL_ARGUMENTS_OFFSET) { + /* PY_VECTORCALL_ARGUMENTS_OFFSET is set, so we are allowed to mutate the vector */ + PyObject **newargs = (PyObject**)args - 1; + nargs += 1; + PyObject *tmp = newargs[0]; + newargs[0] = arg; + assert(newargs[nargs-1]); + result = _PyObject_VectorcallTstate(tstate, callable, newargs, + nargs, kwnames); + newargs[0] = tmp; + } + else { + Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames); + Py_ssize_t totalargs = nargs + nkwargs; + if (totalargs == 0) { + return _PyObject_VectorcallTstate(tstate, callable, &arg, 1, NULL); + } + + PyObject *newargs_stack[_PY_FASTCALL_SMALL_STACK]; + PyObject **newargs; + if (totalargs <= (Py_ssize_t)Py_ARRAY_LENGTH(newargs_stack) - 1) { + newargs = newargs_stack; + } + else { + newargs = PyMem_Malloc((totalargs+1) * sizeof(PyObject *)); + if (newargs == NULL) { + _PyErr_NoMemory(tstate); + return NULL; + } + } + /* use borrowed references */ + newargs[0] = arg; + /* bpo-37138: since totalargs > 0, it's impossible that args is NULL. + * We need this, since calling memcpy() with a NULL pointer is + * undefined behaviour. */ + assert(args != NULL); + memcpy(newargs + 1, args, totalargs * sizeof(PyObject *)); + result = _PyObject_VectorcallTstate(tstate, callable, + newargs, nargs+1, kwnames); + if (newargs != newargs_stack) { + PyMem_Free(newargs); + } + } + return result; +} PyObject * PyObject_VectorcallMethod(PyObject *name, PyObject *const *args, @@ -835,28 +889,44 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args, assert(PyVectorcall_NARGS(nargsf) >= 1); PyThreadState *tstate = _PyThreadState_GET(); - PyObject *callable = NULL; + _PyCStackRef self, method; + _PyThreadState_PushCStackRef(tstate, &self); + _PyThreadState_PushCStackRef(tstate, &method); /* Use args[0] as "self" argument */ - int unbound = _PyObject_GetMethod(args[0], name, &callable); - if (callable == NULL) { + self.ref = PyStackRef_FromPyObjectBorrow(args[0]); + int unbound = _PyObject_GetMethodStackRef(tstate, &self.ref, name, &method.ref); + if (unbound < 0) { + _PyThreadState_PopCStackRef(tstate, &method); + _PyThreadState_PopCStackRef(tstate, &self); return NULL; } - if (unbound) { + PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref); + PyObject *self_obj = PyStackRef_AsPyObjectBorrow(self.ref); + PyObject *result; + + EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_METHOD, callable); + if (self_obj == NULL) { + /* Skip "self". We can keep PY_VECTORCALL_ARGUMENTS_OFFSET since + * args[-1] in the onward call is args[0] here. */ + result = _PyObject_VectorcallTstate(tstate, callable, + args + 1, nargsf - 1, kwnames); + } + else if (self_obj == args[0]) { /* We must remove PY_VECTORCALL_ARGUMENTS_OFFSET since * that would be interpreted as allowing to change args[-1] */ - nargsf &= ~PY_VECTORCALL_ARGUMENTS_OFFSET; + result = _PyObject_VectorcallTstate(tstate, callable, args, + nargsf & ~PY_VECTORCALL_ARGUMENTS_OFFSET, + kwnames); } else { - /* Skip "self". We can keep PY_VECTORCALL_ARGUMENTS_OFFSET since - * args[-1] in the onward call is args[0] here. */ - args++; - nargsf--; + /* classmethod: self_obj is the type, not args[0]. Replace + * args[0] with self_obj and call the underlying callable. */ + result = _PyObject_VectorcallPrepend(tstate, callable, self_obj, + args + 1, nargsf - 1, kwnames); } - EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_METHOD, callable); - PyObject *result = _PyObject_VectorcallTstate(tstate, callable, - args, nargsf, kwnames); - Py_DECREF(callable); + _PyThreadState_PopCStackRef(tstate, &method); + _PyThreadState_PopCStackRef(tstate, &self); return result; } @@ -869,19 +939,26 @@ PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...) return null_error(tstate); } - PyObject *callable = NULL; - int is_method = _PyObject_GetMethod(obj, name, &callable); - if (callable == NULL) { + _PyCStackRef self, method; + _PyThreadState_PushCStackRef(tstate, &self); + _PyThreadState_PushCStackRef(tstate, &method); + self.ref = PyStackRef_FromPyObjectBorrow(obj); + int res = _PyObject_GetMethodStackRef(tstate, &self.ref, name, &method.ref); + if (res < 0) { + _PyThreadState_PopCStackRef(tstate, &method); + _PyThreadState_PopCStackRef(tstate, &self); return NULL; } - obj = is_method ? obj : NULL; + PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref); + PyObject *self_obj = PyStackRef_AsPyObjectBorrow(self.ref); va_list vargs; va_start(vargs, name); - PyObject *result = object_vacall(tstate, obj, callable, vargs); + PyObject *result = object_vacall(tstate, self_obj, callable, vargs); va_end(vargs); - Py_DECREF(callable); + _PyThreadState_PopCStackRef(tstate, &method); + _PyThreadState_PopCStackRef(tstate, &self); return result; } diff --git a/Objects/classobject.c b/Objects/classobject.c index e71f301f2efd77..d511e077be3564 100644 --- a/Objects/classobject.c +++ b/Objects/classobject.c @@ -51,54 +51,7 @@ method_vectorcall(PyObject *method, PyObject *const *args, PyThreadState *tstate = _PyThreadState_GET(); PyObject *self = PyMethod_GET_SELF(method); PyObject *func = PyMethod_GET_FUNCTION(method); - Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); - assert(nargs == 0 || args[nargs-1]); - - PyObject *result; - if (nargsf & PY_VECTORCALL_ARGUMENTS_OFFSET) { - /* PY_VECTORCALL_ARGUMENTS_OFFSET is set, so we are allowed to mutate the vector */ - PyObject **newargs = (PyObject**)args - 1; - nargs += 1; - PyObject *tmp = newargs[0]; - newargs[0] = self; - assert(newargs[nargs-1]); - result = _PyObject_VectorcallTstate(tstate, func, newargs, - nargs, kwnames); - newargs[0] = tmp; - } - else { - Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames); - Py_ssize_t totalargs = nargs + nkwargs; - if (totalargs == 0) { - return _PyObject_VectorcallTstate(tstate, func, &self, 1, NULL); - } - - PyObject *newargs_stack[_PY_FASTCALL_SMALL_STACK]; - PyObject **newargs; - if (totalargs <= (Py_ssize_t)Py_ARRAY_LENGTH(newargs_stack) - 1) { - newargs = newargs_stack; - } - else { - newargs = PyMem_Malloc((totalargs+1) * sizeof(PyObject *)); - if (newargs == NULL) { - _PyErr_NoMemory(tstate); - return NULL; - } - } - /* use borrowed references */ - newargs[0] = self; - /* bpo-37138: since totalargs > 0, it's impossible that args is NULL. - * We need this, since calling memcpy() with a NULL pointer is - * undefined behaviour. */ - assert(args != NULL); - memcpy(newargs + 1, args, totalargs * sizeof(PyObject *)); - result = _PyObject_VectorcallTstate(tstate, func, - newargs, nargs+1, kwnames); - if (newargs != newargs_stack) { - PyMem_Free(newargs); - } - } - return result; + return _PyObject_VectorcallPrepend(tstate, func, self, args, nargsf, kwnames); } diff --git a/Objects/funcobject.c b/Objects/funcobject.c index b870106479a607..ccbc4472206b1c 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -1479,6 +1479,7 @@ cm_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } cm->cm_callable = Py_None; cm->cm_dict = NULL; + _PyObject_SetDeferredRefcount((PyObject *)cm); return (PyObject *)cm; } @@ -1722,6 +1723,7 @@ sm_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } sm->sm_callable = Py_None; sm->sm_dict = NULL; + _PyObject_SetDeferredRefcount((PyObject *)sm); return (PyObject *)sm; } @@ -1889,3 +1891,17 @@ PyStaticMethod_New(PyObject *callable) } return (PyObject *)sm; } + +PyObject * +_PyClassMethod_GetFunc(PyObject *self) +{ + classmethod *cm = _PyClassMethod_CAST(self); + return cm->cm_callable; +} + +PyObject * +_PyStaticMethod_GetFunc(PyObject *self) +{ + staticmethod *sm = _PyStaticMethod_CAST(self); + return sm->sm_callable; +} diff --git a/Objects/object.c b/Objects/object.c index b82599b9c4fd5f..e91e0f1e8b7219 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -10,6 +10,7 @@ #include "pycore_descrobject.h" // _PyMethodWrapper_Type #include "pycore_dict.h" // _PyObject_MaterializeManagedDict() #include "pycore_floatobject.h" // _PyFloat_DebugMallocStats() +#include "pycore_function.h" // _PyClassMethod_GetFunc() #include "pycore_freelist.h" // _PyObject_ClearFreeLists() #include "pycore_genobject.h" // _PyAsyncGenAThrow_Type #include "pycore_hamt.h" // _PyHamtItems_Type @@ -1664,6 +1665,142 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) return 0; } +// Look up a method on `self` by `name`. +// +// On success, `*method` is set and the function returns 0 or 1. If the +// return value is 1, the call is an unbound method and `*self` is the +// "self" or "cls" argument to pass. If the return value is 0, the call is +// a regular function and `*self` is cleared. +// +// On error, returns -1, clears `*self`, and sets an exception. +int +_PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, + PyObject *name, _PyStackRef *method) +{ + int meth_found = 0; + PyObject *obj = PyStackRef_AsPyObjectBorrow(*self); + + assert(PyStackRef_IsNull(*method)); + + PyTypeObject *tp = Py_TYPE(obj); + if (!_PyType_IsReady(tp)) { + if (PyType_Ready(tp) < 0) { + PyStackRef_CLEAR(*self); + return -1; + } + } + + if (tp->tp_getattro != PyObject_GenericGetAttr || !PyUnicode_CheckExact(name)) { + PyObject *res = PyObject_GetAttr(obj, name); + PyStackRef_CLEAR(*self); + if (res != NULL) { + *method = PyStackRef_FromPyObjectSteal(res); + return 0; + } + return -1; + } + + _PyType_LookupStackRefAndVersion(tp, name, method); + PyObject *descr = PyStackRef_AsPyObjectBorrow(*method); + descrgetfunc f = NULL; + if (descr != NULL) { + if (_PyType_HasFeature(Py_TYPE(descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) { + meth_found = 1; + } + else { + f = Py_TYPE(descr)->tp_descr_get; + if (f != NULL && PyDescr_IsData(descr)) { + PyObject *value = f(descr, obj, (PyObject *)Py_TYPE(obj)); + PyStackRef_CLEAR(*method); + PyStackRef_CLEAR(*self); + if (value != NULL) { + *method = PyStackRef_FromPyObjectSteal(value); + return 0; + } + return -1; + } + } + } + PyObject *dict, *attr; + if ((tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) && + _PyObject_TryGetInstanceAttribute(obj, name, &attr)) { + if (attr != NULL) { + PyStackRef_XSETREF(*method, PyStackRef_FromPyObjectSteal(attr)); + PyStackRef_CLEAR(*self); + return 0; + } + dict = NULL; + } + else if ((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT)) { + dict = (PyObject *)_PyObject_GetManagedDict(obj); + } + else { + PyObject **dictptr = _PyObject_ComputedDictPointer(obj); + if (dictptr != NULL) { + dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*dictptr); + } + else { + dict = NULL; + } + } + if (dict != NULL) { + assert(PyUnicode_CheckExact(name)); + int found = _PyDict_GetMethodStackRef((PyDictObject *)dict, name, method); + if (found < 0) { + assert(PyStackRef_IsNull(*method)); + PyStackRef_CLEAR(*self); + return -1; + } + else if (found) { + PyStackRef_CLEAR(*self); + return 0; + } + } + + if (meth_found) { + assert(!PyStackRef_IsNull(*method)); + return 1; + } + + if (f != NULL) { + if (Py_IS_TYPE(descr, &PyClassMethod_Type)) { + PyObject *callable = _PyClassMethod_GetFunc(descr); + PyStackRef_XSETREF(*method, PyStackRef_FromPyObjectNew(callable)); + PyStackRef_XSETREF(*self, PyStackRef_FromPyObjectNew((PyObject *)tp)); + return 1; + } + else if (Py_IS_TYPE(descr, &PyStaticMethod_Type)) { + PyObject *callable = _PyStaticMethod_GetFunc(descr); + PyStackRef_XSETREF(*method, PyStackRef_FromPyObjectNew(callable)); + PyStackRef_CLEAR(*self); + return 0; + } + PyObject *value = f(descr, obj, (PyObject *)tp); + PyStackRef_CLEAR(*method); + PyStackRef_CLEAR(*self); + if (value) { + *method = PyStackRef_FromPyObjectSteal(value); + return 0; + } + return -1; + } + + if (descr != NULL) { + assert(!PyStackRef_IsNull(*method)); + PyStackRef_CLEAR(*self); + return 0; + } + + PyErr_Format(PyExc_AttributeError, + "'%.100s' object has no attribute '%U'", + tp->tp_name, name); + + _PyObject_SetAttributeErrorContext(obj, name); + assert(PyStackRef_IsNull(*method)); + PyStackRef_CLEAR(*self); + return -1; +} + /* Generic GetAttr functions - put these in your tp_[gs]etattro slot. */ PyObject * diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 78325111374a3f..fe49a3fd05dda4 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2292,39 +2292,19 @@ dummy_func( op(_LOAD_ATTR, (owner -- attr, self_or_null[oparg&1])) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1); - PyObject *attr_o; if (oparg & 1) { /* Designed to work in tandem with CALL, pushes two values. */ - attr_o = NULL; - int is_meth = _PyObject_GetMethod(PyStackRef_AsPyObjectBorrow(owner), name, &attr_o); - if (is_meth) { - /* We can bypass temporary bound method object. - meth is unbound method and obj is self. - meth | self | arg1 | ... | argN - */ - assert(attr_o != NULL); // No errors on this branch - self_or_null[0] = owner; // Transfer ownership - DEAD(owner); - } - else { - /* meth is not an unbound method (but a regular attr, or - something was returned by a descriptor protocol). Set - the second element of the stack to NULL, to signal - CALL that it's not a method call. - meth | NULL | arg1 | ... | argN - */ - PyStackRef_CLOSE(owner); - ERROR_IF(attr_o == NULL); - self_or_null[0] = PyStackRef_NULL; - } + attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null); + DEAD(owner); + ERROR_IF(PyStackRef_IsNull(attr)); } else { /* Classic, pushes one value. */ - attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); + PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); PyStackRef_CLOSE(owner); ERROR_IF(attr_o == NULL); + attr = PyStackRef_FromPyObjectSteal(attr_o); } - attr = PyStackRef_FromPyObjectSteal(attr_o); } macro(LOAD_ATTR) = diff --git a/Python/ceval.c b/Python/ceval.c index e6d82f249ef550..94a095ccd90f73 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -736,15 +736,19 @@ _PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys) PyObject *seen = NULL; PyObject *dummy = NULL; PyObject *values = NULL; - PyObject *get = NULL; // We use the two argument form of map.get(key, default) for two reasons: // - Atomically check for a key and get its value without error handling. // - Don't cause key creation or resizing in dict subclasses like // collections.defaultdict that define __missing__ (or similar). - int meth_found = _PyObject_GetMethod(map, &_Py_ID(get), &get); - if (get == NULL) { + _PyCStackRef self, method; + _PyThreadState_PushCStackRef(tstate, &self); + _PyThreadState_PushCStackRef(tstate, &method); + self.ref = PyStackRef_FromPyObjectBorrow(map); + int res = _PyObject_GetMethodStackRef(tstate, &self.ref, &_Py_ID(get), &method.ref); + if (res < 0) { goto fail; } + PyObject *get = PyStackRef_AsPyObjectBorrow(method.ref); seen = PySet_New(NULL); if (seen == NULL) { goto fail; @@ -768,9 +772,10 @@ _PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys) } goto fail; } - PyObject *args[] = { map, key, dummy }; + PyObject *self_obj = PyStackRef_AsPyObjectBorrow(self.ref); + PyObject *args[] = { self_obj, key, dummy }; PyObject *value = NULL; - if (meth_found) { + if (!PyStackRef_IsNull(self.ref)) { value = PyObject_Vectorcall(get, args, 3, NULL); } else { @@ -791,12 +796,14 @@ _PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys) } // Success: done: - Py_DECREF(get); + _PyThreadState_PopCStackRef(tstate, &method); + _PyThreadState_PopCStackRef(tstate, &self); Py_DECREF(seen); Py_DECREF(dummy); return values; fail: - Py_XDECREF(get); + _PyThreadState_PopCStackRef(tstate, &method); + _PyThreadState_PopCStackRef(tstate, &self); Py_XDECREF(seen); Py_XDECREF(dummy); Py_XDECREF(values); @@ -997,6 +1004,26 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) #include "ceval_macros.h" + +_PyStackRef +_Py_LoadAttr_StackRefSteal( + PyThreadState *tstate, _PyStackRef owner, + PyObject *name, _PyStackRef *self_or_null) +{ + // Use _PyCStackRefs to ensure that both method and self are visible to + // the GC. Even though self_or_null is on the evaluation stack, it may be + // after the stackpointer and therefore not visible to the GC. + _PyCStackRef method, self; + _PyThreadState_PushCStackRef(tstate, &method); + _PyThreadState_PushCStackRef(tstate, &self); + self.ref = owner; // steal reference to owner + // NOTE: method.ref is initialized to PyStackRef_NULL and remains null on + // error, so we don't need to explicitly use the return code from the call. + _PyObject_GetMethodStackRef(tstate, &self.ref, name, &method.ref); + *self_or_null = _PyThreadState_PopCStackRefSteal(tstate, &self); + return _PyThreadState_PopCStackRefSteal(tstate, &method); +} + int _Py_CheckRecursiveCallPy( PyThreadState *tstate) { diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 9dcd9afe884153..934c868bb64735 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -3190,32 +3190,20 @@ owner = stack_pointer[-1]; self_or_null = &stack_pointer[0]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1); - PyObject *attr_o; if (oparg & 1) { - attr_o = NULL; _PyFrame_SetStackPointer(frame, stack_pointer); - int is_meth = _PyObject_GetMethod(PyStackRef_AsPyObjectBorrow(owner), name, &attr_o); + attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null); stack_pointer = _PyFrame_GetStackPointer(frame); - if (is_meth) { - assert(attr_o != NULL); - self_or_null[0] = owner; - } - else { - stack_pointer += -1; + if (PyStackRef_IsNull(attr)) { + stack_pointer[-1] = attr; + stack_pointer += (oparg&1); assert(WITHIN_STACK_BOUNDS()); - _PyFrame_SetStackPointer(frame, stack_pointer); - PyStackRef_CLOSE(owner); - stack_pointer = _PyFrame_GetStackPointer(frame); - if (attr_o == NULL) { - JUMP_TO_ERROR(); - } - self_or_null[0] = PyStackRef_NULL; - stack_pointer += 1; + JUMP_TO_ERROR(); } } else { _PyFrame_SetStackPointer(frame, stack_pointer); - attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); + PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); stack_pointer = _PyFrame_GetStackPointer(frame); stack_pointer += -1; assert(WITHIN_STACK_BOUNDS()); @@ -3225,9 +3213,9 @@ if (attr_o == NULL) { JUMP_TO_ERROR(); } + attr = PyStackRef_FromPyObjectSteal(attr_o); stack_pointer += 1; } - attr = PyStackRef_FromPyObjectSteal(attr_o); stack_pointer[-1] = attr; stack_pointer += (oparg&1); assert(WITHIN_STACK_BOUNDS()); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 20b5c4b3f497a9..c0cc12929c2405 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -8015,32 +8015,17 @@ { self_or_null = &stack_pointer[0]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1); - PyObject *attr_o; if (oparg & 1) { - attr_o = NULL; _PyFrame_SetStackPointer(frame, stack_pointer); - int is_meth = _PyObject_GetMethod(PyStackRef_AsPyObjectBorrow(owner), name, &attr_o); + attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null); stack_pointer = _PyFrame_GetStackPointer(frame); - if (is_meth) { - assert(attr_o != NULL); - self_or_null[0] = owner; - } - else { - stack_pointer += -1; - assert(WITHIN_STACK_BOUNDS()); - _PyFrame_SetStackPointer(frame, stack_pointer); - PyStackRef_CLOSE(owner); - stack_pointer = _PyFrame_GetStackPointer(frame); - if (attr_o == NULL) { - JUMP_TO_LABEL(error); - } - self_or_null[0] = PyStackRef_NULL; - stack_pointer += 1; + if (PyStackRef_IsNull(attr)) { + JUMP_TO_LABEL(pop_1_error); } } else { _PyFrame_SetStackPointer(frame, stack_pointer); - attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); + PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); stack_pointer = _PyFrame_GetStackPointer(frame); stack_pointer += -1; assert(WITHIN_STACK_BOUNDS()); @@ -8050,9 +8035,9 @@ if (attr_o == NULL) { JUMP_TO_LABEL(error); } + attr = PyStackRef_FromPyObjectSteal(attr_o); stack_pointer += 1; } - attr = PyStackRef_FromPyObjectSteal(attr_o); } stack_pointer[-1] = attr; stack_pointer += (oparg&1); diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index cc7d8575f5cfc9..9c86822418c892 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -218,6 +218,28 @@ def method(self): obj.method() +class MyClassMethod: + @classmethod + def my_classmethod(cls): + return cls + + @staticmethod + def my_staticmethod(): + pass + +@register_benchmark +def classmethod_call(): + obj = MyClassMethod() + for _ in range(1000 * WORK_SCALE): + obj.my_classmethod() + +@register_benchmark +def staticmethod_call(): + obj = MyClassMethod() + for _ in range(1000 * WORK_SCALE): + obj.my_staticmethod() + + def bench_one_thread(func): t0 = time.perf_counter_ns() func()