https://github.com/python/cpython/commit/fa3143a1d2d6d241632c835cbe8fc541adf60f68
commit: fa3143a1d2d6d241632c835cbe8fc541adf60f68
branch: 3.14
author: Sam Gross <[email protected]>
committer: colesbury <[email protected]>
date: 2026-03-19T10:49:12-04:00
summary:

[3.14] gh-145779: Improve classmethod/staticmethod scaling in free-threaded 
build (gh-145826) (#146088)

Add special cases for classmethod and staticmethod descriptors in
_PyObject_GetMethodStackRef() to avoid calling tp_descr_get, which
avoids reference count contention on the bound method and underlying
callable. This improves scaling when calling classmethods and
staticmethods from multiple threads.

Also refactor method_vectorcall in classobject.c into a new
_PyObject_VectorcallPrepend() helper so that it can be used by
PyObject_VectorcallMethod as well.

(cherry picked from commit e0f7c1097e19b6f5c2399e19f283c9fb373c243f)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-03-10-22-38-40.gh-issue-145779.5375381d80.rst
M Include/internal/pycore_call.h
M Include/internal/pycore_ceval.h
M Include/internal/pycore_function.h
M Include/internal/pycore_object.h
M Include/internal/pycore_stackref.h
M Objects/call.c
M Objects/classobject.c
M Objects/funcobject.c
M Objects/object.c
M Python/bytecodes.c
M Python/ceval.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h
M Tools/ftscalingbench/ftscalingbench.py

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 7a33f63a051780..a477fdd51ec5a5 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -2295,39 +2295,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 b48939c0547b16..59c39214405689 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 cef14cbf930ed1..552cfac2dbef28 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -3189,32 +3189,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());
@@ -3224,9 +3212,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 f6dec81af265e2..5cba25d5c7d385 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -8014,32 +8014,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());
@@ -8049,9 +8034,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()

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to