Skip to content

Commit ab0e275

Browse files
eendebakptclaude
andcommitted
Use _PyObject_GetMethodStackRef so method calls scale in free-threading
Resolve the method in callmethod_va() via _PyObject_GetMethodStackRef() instead of _PyObject_GetMethod(). The StackRef variant returns the method as a deferred reference, avoiding the per-call atomic refcount on the shared method object that otherwise serializes threads in the free-threaded build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8de28e4 commit ab0e275

1 file changed

Lines changed: 29 additions & 19 deletions

File tree

Objects/call.c

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -612,27 +612,36 @@ _PyObject_CallFunction_SizeT(PyObject *callable, const char *format, ...)
612612
}
613613

614614

615-
/* Resolve 'name' on 'obj' with _PyObject_GetMethod and call it directly,
616-
avoiding the bound-method object that PyObject_GetAttr()+call would allocate. */
615+
/* Resolve 'name' on 'obj' with _PyObject_GetMethodStackRef and call it
616+
directly, avoiding the bound-method object that PyObject_GetAttr()+call
617+
would allocate. Using the StackRef variant keeps method resolution
618+
reference-count-free on the fast path so it scales in free-threading. */
617619
static PyObject *
618620
callmethod_va(PyObject *obj, PyObject *name,
619621
const char *format, va_list va)
620622
{
621623
PyThreadState *tstate = _PyThreadState_GET();
624+
PyObject *result = NULL;
622625

623-
PyObject *method = NULL;
624-
/* unbound: 1 -> 'method' is an unbound function, call method(obj, *args);
625-
0 -> 'method' is the resolved attribute, call method(*args). */
626-
int unbound = _PyObject_GetMethod(obj, name, &method);
627-
if (method == NULL) {
628-
return NULL;
626+
_PyCStackRef self, method;
627+
_PyThreadState_PushCStackRef(tstate, &self);
628+
_PyThreadState_PushCStackRef(tstate, &method);
629+
self.ref = PyStackRef_FromPyObjectBorrow(obj);
630+
/* On return, self.ref is non-NULL -> call method(self, *args) (unbound
631+
method or classmethod), NULL -> call method(*args). */
632+
int res = _PyObject_GetMethodStackRef(tstate, &self.ref, name, &method.ref);
633+
if (res < 0) {
634+
goto pop_return;
629635
}
630-
if (!PyCallable_Check(method)) {
636+
637+
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
638+
PyObject *self_obj = PyStackRef_AsPyObjectBorrow(self.ref);
639+
640+
if (!PyCallable_Check(callable)) {
631641
_PyErr_Format(tstate, PyExc_TypeError,
632642
"attribute of type '%.200s' is not callable",
633-
Py_TYPE(method)->tp_name);
634-
Py_DECREF(method);
635-
return NULL;
643+
Py_TYPE(callable)->tp_name);
644+
goto pop_return;
636645
}
637646

638647
/* Build the positional arguments from the format string. */
@@ -643,8 +652,7 @@ callmethod_va(PyObject *obj, PyObject *name,
643652
built = _Py_VaBuildStack(small_stack, _PY_FASTCALL_SMALL_STACK,
644653
format, va, &nargs);
645654
if (built == NULL) {
646-
Py_DECREF(method);
647-
return NULL;
655+
goto pop_return;
648656
}
649657
}
650658

@@ -656,12 +664,11 @@ callmethod_va(PyObject *obj, PyObject *name,
656664
n = PyTuple_GET_SIZE(built[0]);
657665
}
658666

659-
PyObject *result;
660-
if (unbound) {
661-
result = _PyObject_VectorcallPrepend(tstate, method, obj, args, n, NULL);
667+
if (self_obj != NULL) {
668+
result = _PyObject_VectorcallPrepend(tstate, callable, self_obj, args, n, NULL);
662669
}
663670
else {
664-
result = _PyObject_VectorcallTstate(tstate, method, args, n, NULL);
671+
result = _PyObject_VectorcallTstate(tstate, callable, args, n, NULL);
665672
}
666673

667674
for (Py_ssize_t i = 0; i < nargs; i++) {
@@ -670,7 +677,10 @@ callmethod_va(PyObject *obj, PyObject *name,
670677
if (built != NULL && built != small_stack) {
671678
PyMem_Free(built);
672679
}
673-
Py_DECREF(method);
680+
681+
pop_return:
682+
_PyThreadState_PopCStackRef(tstate, &method);
683+
_PyThreadState_PopCStackRef(tstate, &self);
674684
return result;
675685
}
676686

0 commit comments

Comments
 (0)