From 8de28e4f5c8d3f0b160341de26c51918e5925a1c Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 8 Jun 2026 15:31:22 +0200 Subject: [PATCH 1/3] Speed up PyObject_CallMethod via _PyObject_GetMethod PyObject_CallMethod (and _PyObject_CallMethod, PyEval_CallMethod, _PyObject_CallMethodId and the _SizeT variant) resolved the method with PyObject_GetAttr, which builds a temporary bound-method object on every call, then called it. Resolve the method with _PyObject_GetMethod instead (the same lookup the interpreter uses for obj.name(...)) and call it directly via _PyObject_VectorcallPrepend, skipping the bound-method allocation. Behaviour is unchanged: same attribute semantics, same "attribute of type ... is not callable" error, and the historical PyObject_CallMethod(o, m, "O", tuple) -> o.m(*tuple) unpacking. The shared helper callmethod() and _PyObject_CallMethodFormat() are no longer needed: their only caller (traceback.c) now uses PyObject_CallFunction, so both are removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- Include/internal/pycore_call.h | 6 -- Objects/call.c | 111 +++++++++++++++++++++------------ Python/traceback.c | 5 +- 3 files changed, 72 insertions(+), 50 deletions(-) diff --git a/Include/internal/pycore_call.h b/Include/internal/pycore_call.h index a9db8860e91c06..2c2bd0de9e1e14 100644 --- a/Include/internal/pycore_call.h +++ b/Include/internal/pycore_call.h @@ -52,12 +52,6 @@ extern PyObject* _PyObject_Call( PyObject *args, PyObject *kwargs); -extern PyObject * _PyObject_CallMethodFormat( - PyThreadState *tstate, - PyObject *callable, - const char *format, - ...); - // Export for 'array' shared extension PyAPI_FUNC(PyObject*) _PyObject_CallMethod( PyObject *obj, diff --git a/Objects/call.c b/Objects/call.c index 9718642473103c..e7f9d40532d8c1 100644 --- a/Objects/call.c +++ b/Objects/call.c @@ -612,18 +612,66 @@ _PyObject_CallFunction_SizeT(PyObject *callable, const char *format, ...) } -static PyObject* -callmethod(PyThreadState *tstate, PyObject* callable, const char *format, va_list va) +/* Resolve 'name' on 'obj' with _PyObject_GetMethod and call it directly, + avoiding the bound-method object that PyObject_GetAttr()+call would allocate. */ +static PyObject * +callmethod_va(PyObject *obj, PyObject *name, + const char *format, va_list va) { - assert(callable != NULL); - if (!PyCallable_Check(callable)) { + PyThreadState *tstate = _PyThreadState_GET(); + + PyObject *method = NULL; + /* unbound: 1 -> 'method' is an unbound function, call method(obj, *args); + 0 -> 'method' is the resolved attribute, call method(*args). */ + int unbound = _PyObject_GetMethod(obj, name, &method); + if (method == NULL) { + return NULL; + } + if (!PyCallable_Check(method)) { _PyErr_Format(tstate, PyExc_TypeError, "attribute of type '%.200s' is not callable", - Py_TYPE(callable)->tp_name); + Py_TYPE(method)->tp_name); + Py_DECREF(method); return NULL; } - return _PyObject_CallFunctionVa(tstate, callable, format, va); + /* Build the positional arguments from the format string. */ + PyObject *small_stack[_PY_FASTCALL_SMALL_STACK]; + Py_ssize_t nargs = 0; + PyObject **built = NULL; + if (format != NULL && *format != '\0') { + built = _Py_VaBuildStack(small_stack, _PY_FASTCALL_SMALL_STACK, + format, va, &nargs); + if (built == NULL) { + Py_DECREF(method); + return NULL; + } + } + + /* Backward compat: a single tuple from "O" is unpacked. */ + PyObject *const *args = built; + Py_ssize_t n = nargs; + if (nargs == 1 && PyTuple_Check(built[0])) { + args = _PyTuple_ITEMS(built[0]); + n = PyTuple_GET_SIZE(built[0]); + } + + PyObject *result; + if (unbound) { + result = _PyObject_VectorcallPrepend(tstate, method, obj, args, n, NULL); + } + else { + result = _PyObject_VectorcallTstate(tstate, method, args, n, NULL); + } + + for (Py_ssize_t i = 0; i < nargs; i++) { + Py_DECREF(built[i]); + } + if (built != NULL && built != small_stack) { + PyMem_Free(built); + } + Py_DECREF(method); + return result; } PyObject * @@ -635,17 +683,17 @@ PyObject_CallMethod(PyObject *obj, const char *name, const char *format, ...) return null_error(tstate); } - PyObject *callable = PyObject_GetAttrString(obj, name); - if (callable == NULL) { + PyObject *name_obj = PyUnicode_FromString(name); + if (name_obj == NULL) { return NULL; } va_list va; va_start(va, format); - PyObject *retval = callmethod(tstate, callable, format, va); + PyObject *retval = callmethod_va(obj, name_obj, format, va); va_end(va); - Py_DECREF(callable); + Py_DECREF(name_obj); return retval; } @@ -660,17 +708,17 @@ PyEval_CallMethod(PyObject *obj, const char *name, const char *format, ...) return null_error(tstate); } - PyObject *callable = PyObject_GetAttrString(obj, name); - if (callable == NULL) { + PyObject *name_obj = PyUnicode_FromString(name); + if (name_obj == NULL) { return NULL; } va_list va; va_start(va, format); - PyObject *retval = callmethod(tstate, callable, format, va); + PyObject *retval = callmethod_va(obj, name_obj, format, va); va_end(va); - Py_DECREF(callable); + Py_DECREF(name_obj); return retval; } @@ -684,17 +732,11 @@ _PyObject_CallMethod(PyObject *obj, PyObject *name, return null_error(tstate); } - PyObject *callable = PyObject_GetAttr(obj, name); - if (callable == NULL) { - return NULL; - } - va_list va; va_start(va, format); - PyObject *retval = callmethod(tstate, callable, format, va); + PyObject *retval = callmethod_va(obj, name, format, va); va_end(va); - Py_DECREF(callable); return retval; } @@ -710,30 +752,17 @@ _PyObject_CallMethodId(PyObject *obj, _Py_Identifier *name, _Py_COMP_DIAG_PUSH _Py_COMP_DIAG_IGNORE_DEPR_DECLS - PyObject *callable = _PyObject_GetAttrId(obj, name); + PyObject *name_obj = _PyUnicode_FromId(name); /* borrowed */ _Py_COMP_DIAG_POP - if (callable == NULL) { + if (name_obj == NULL) { return NULL; } va_list va; va_start(va, format); - PyObject *retval = callmethod(tstate, callable, format, va); + PyObject *retval = callmethod_va(obj, name_obj, format, va); va_end(va); - Py_DECREF(callable); - return retval; -} - - -PyObject * _PyObject_CallMethodFormat(PyThreadState *tstate, PyObject *callable, - const char *format, ...) -{ - assert(callable != NULL); - va_list va; - va_start(va, format); - PyObject *retval = callmethod(tstate, callable, format, va); - va_end(va); return retval; } @@ -749,17 +778,17 @@ _PyObject_CallMethod_SizeT(PyObject *obj, const char *name, return null_error(tstate); } - PyObject *callable = PyObject_GetAttrString(obj, name); - if (callable == NULL) { + PyObject *name_obj = PyUnicode_FromString(name); + if (name_obj == NULL) { return NULL; } va_list va; va_start(va, format); - PyObject *retval = callmethod(tstate, callable, format, va); + PyObject *retval = callmethod_va(obj, name_obj, format, va); va_end(va); - Py_DECREF(callable); + Py_DECREF(name_obj); return retval; } diff --git a/Python/traceback.c b/Python/traceback.c index 50a79d78d2e10e..1fc940b8c2794c 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -2,7 +2,7 @@ /* Traceback implementation */ #include "Python.h" -#include "pycore_call.h" // _PyObject_CallMethodFormat() +#include "pycore_call.h" // _PyObject_CallMethod() #include "pycore_fileutils.h" // _Py_BEGIN_SUPPRESS_IPH #include "pycore_frame.h" // PyFrameObject #include "pycore_interp.h" // PyInterpreterState.gc @@ -404,7 +404,6 @@ _Py_FindSourceFile(PyObject *filename, char* namebuf, size_t namelen, PyObject * tail++; taillen = strlen(tail); - PyThreadState *tstate = _PyThreadState_GET(); if (PySys_GetOptionalAttr(&_Py_ID(path), &syspath) < 0) { PyErr_Clear(); goto error; @@ -444,7 +443,7 @@ _Py_FindSourceFile(PyObject *filename, char* namebuf, size_t namelen, PyObject * namebuf[len++] = SEP; strcpy(namebuf+len, tail); - binary = _PyObject_CallMethodFormat(tstate, open, "ss", namebuf, "rb"); + binary = PyObject_CallFunction(open, "ss", namebuf, "rb"); if (binary != NULL) { result = binary; goto finally; From ab0e27553f09ba6ac1db80917d6680d391e5e72c Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 11 Jun 2026 13:39:51 +0200 Subject: [PATCH 2/3] 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) --- Objects/call.c | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/Objects/call.c b/Objects/call.c index e7f9d40532d8c1..9c8ebcd025113b 100644 --- a/Objects/call.c +++ b/Objects/call.c @@ -612,27 +612,36 @@ _PyObject_CallFunction_SizeT(PyObject *callable, const char *format, ...) } -/* Resolve 'name' on 'obj' with _PyObject_GetMethod and call it directly, - avoiding the bound-method object that PyObject_GetAttr()+call would allocate. */ +/* Resolve 'name' on 'obj' with _PyObject_GetMethodStackRef and call it + directly, avoiding the bound-method object that PyObject_GetAttr()+call + would allocate. Using the StackRef variant keeps method resolution + reference-count-free on the fast path so it scales in free-threading. */ static PyObject * callmethod_va(PyObject *obj, PyObject *name, const char *format, va_list va) { PyThreadState *tstate = _PyThreadState_GET(); + PyObject *result = NULL; - PyObject *method = NULL; - /* unbound: 1 -> 'method' is an unbound function, call method(obj, *args); - 0 -> 'method' is the resolved attribute, call method(*args). */ - int unbound = _PyObject_GetMethod(obj, name, &method); - if (method == NULL) { - return NULL; + _PyCStackRef self, method; + _PyThreadState_PushCStackRef(tstate, &self); + _PyThreadState_PushCStackRef(tstate, &method); + self.ref = PyStackRef_FromPyObjectBorrow(obj); + /* On return, self.ref is non-NULL -> call method(self, *args) (unbound + method or classmethod), NULL -> call method(*args). */ + int res = _PyObject_GetMethodStackRef(tstate, &self.ref, name, &method.ref); + if (res < 0) { + goto pop_return; } - if (!PyCallable_Check(method)) { + + PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref); + PyObject *self_obj = PyStackRef_AsPyObjectBorrow(self.ref); + + if (!PyCallable_Check(callable)) { _PyErr_Format(tstate, PyExc_TypeError, "attribute of type '%.200s' is not callable", - Py_TYPE(method)->tp_name); - Py_DECREF(method); - return NULL; + Py_TYPE(callable)->tp_name); + goto pop_return; } /* Build the positional arguments from the format string. */ @@ -643,8 +652,7 @@ callmethod_va(PyObject *obj, PyObject *name, built = _Py_VaBuildStack(small_stack, _PY_FASTCALL_SMALL_STACK, format, va, &nargs); if (built == NULL) { - Py_DECREF(method); - return NULL; + goto pop_return; } } @@ -656,12 +664,11 @@ callmethod_va(PyObject *obj, PyObject *name, n = PyTuple_GET_SIZE(built[0]); } - PyObject *result; - if (unbound) { - result = _PyObject_VectorcallPrepend(tstate, method, obj, args, n, NULL); + if (self_obj != NULL) { + result = _PyObject_VectorcallPrepend(tstate, callable, self_obj, args, n, NULL); } else { - result = _PyObject_VectorcallTstate(tstate, method, args, n, NULL); + result = _PyObject_VectorcallTstate(tstate, callable, args, n, NULL); } for (Py_ssize_t i = 0; i < nargs; i++) { @@ -670,7 +677,10 @@ callmethod_va(PyObject *obj, PyObject *name, if (built != NULL && built != small_stack) { PyMem_Free(built); } - Py_DECREF(method); + +pop_return: + _PyThreadState_PopCStackRef(tstate, &method); + _PyThreadState_PopCStackRef(tstate, &self); return result; } From a6f9e4f8cfdd89799b0563e57f476ef2b2e8b992 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 11 Jun 2026 20:47:49 +0200 Subject: [PATCH 3/3] rename to callmethod --- Objects/call.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Objects/call.c b/Objects/call.c index 9c8ebcd025113b..c701cdfb72e44f 100644 --- a/Objects/call.c +++ b/Objects/call.c @@ -617,7 +617,7 @@ _PyObject_CallFunction_SizeT(PyObject *callable, const char *format, ...) would allocate. Using the StackRef variant keeps method resolution reference-count-free on the fast path so it scales in free-threading. */ static PyObject * -callmethod_va(PyObject *obj, PyObject *name, +callmethod(PyObject *obj, PyObject *name, const char *format, va_list va) { PyThreadState *tstate = _PyThreadState_GET(); @@ -700,7 +700,7 @@ PyObject_CallMethod(PyObject *obj, const char *name, const char *format, ...) va_list va; va_start(va, format); - PyObject *retval = callmethod_va(obj, name_obj, format, va); + PyObject *retval = callmethod(obj, name_obj, format, va); va_end(va); Py_DECREF(name_obj); @@ -725,7 +725,7 @@ PyEval_CallMethod(PyObject *obj, const char *name, const char *format, ...) va_list va; va_start(va, format); - PyObject *retval = callmethod_va(obj, name_obj, format, va); + PyObject *retval = callmethod(obj, name_obj, format, va); va_end(va); Py_DECREF(name_obj); @@ -744,7 +744,7 @@ _PyObject_CallMethod(PyObject *obj, PyObject *name, va_list va; va_start(va, format); - PyObject *retval = callmethod_va(obj, name, format, va); + PyObject *retval = callmethod(obj, name, format, va); va_end(va); return retval; @@ -770,7 +770,7 @@ _Py_COMP_DIAG_POP va_list va; va_start(va, format); - PyObject *retval = callmethod_va(obj, name_obj, format, va); + PyObject *retval = callmethod(obj, name_obj, format, va); va_end(va); return retval; @@ -795,7 +795,7 @@ _PyObject_CallMethod_SizeT(PyObject *obj, const char *name, va_list va; va_start(va, format); - PyObject *retval = callmethod_va(obj, name_obj, format, va); + PyObject *retval = callmethod(obj, name_obj, format, va); va_end(va); Py_DECREF(name_obj);