[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 e0f7c1097e)
This commit is contained in:
Sam Gross 2026-03-19 10:49:12 -04:00 committed by GitHub
parent 7f29c1d0da
commit fa3143a1d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 387 additions and 140 deletions

View file

@ -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) =

View file

@ -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)
{

View file

@ -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());

View file

@ -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);