https://github.com/python/cpython/commit/3f9118f1d17ae2ff0e25bd07fce9940ba13afdff
commit: 3f9118f1d17ae2ff0e25bd07fce9940ba13afdff
branch: main
author: Pieter Eendebak <[email protected]>
committer: eendebakpt <[email protected]>
date: 2026-10-05T18:44:28Z
summary:

gh-151157: Speed up PyObject_CallMethod via _PyObject_GetMethod (#151158)

* 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) <[email protected]>

* 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) <[email protected]>

* rename to callmethod

* review comments

* review comments

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Co-authored-by: Kumar Aditya <[email protected]>
Co-authored-by: Inada Naoki <[email protected]>

files:
M Include/internal/pycore_call.h
M Objects/call.c
M Python/traceback.c

diff --git a/Include/internal/pycore_call.h b/Include/internal/pycore_call.h
index 477b3c20e53f6e..824bb7f9499a9a 100644
--- a/Include/internal/pycore_call.h
+++ b/Include/internal/pycore_call.h
@@ -57,12 +57,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..a79f167aacc8c0 100644
--- a/Objects/call.c
+++ b/Objects/call.c
@@ -612,18 +612,70 @@ _PyObject_CallFunction_SizeT(PyObject *callable, const 
char *format, ...)
 }
 
 
-static PyObject*
-callmethod(PyThreadState *tstate, PyObject* callable, const char *format, 
va_list va)
+static PyObject *
+callmethod(PyThreadState *tstate, PyObject *obj, PyObject *name,
+           const char *format, va_list va)
 {
-    assert(callable != NULL);
+    PyObject *result = 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) {
+        goto exit;
+    }
+
+    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(callable)->tp_name);
-        return NULL;
+        goto exit;
     }
 
-    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) {
+            goto exit;
+        }
+    }
+
+    /* 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]);
+    }
+
+    if (self_obj != NULL) {
+        result = _PyObject_VectorcallPrepend(tstate, callable, self_obj, args, 
n, NULL);
+    }
+    else {
+        result = _PyObject_VectorcallTstate(tstate, callable, args, n, NULL);
+    }
+
+    for (Py_ssize_t i = 0; i < nargs; i++) {
+        Py_DECREF(built[i]);
+    }
+    if (built != small_stack) {
+        PyMem_Free(built);
+    }
+
+exit:
+    _PyThreadState_PopCStackRef(tstate, &method);
+    _PyThreadState_PopCStackRef(tstate, &self);
+    return result;
 }
 
 PyObject *
@@ -635,17 +687,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(tstate, obj, name_obj, format, va);
     va_end(va);
 
-    Py_DECREF(callable);
+    Py_DECREF(name_obj);
     return retval;
 }
 
@@ -660,17 +712,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(tstate, obj, name_obj, format, va);
     va_end(va);
 
-    Py_DECREF(callable);
+    Py_DECREF(name_obj);
     return retval;
 }
 
@@ -684,17 +736,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(tstate, obj, name, format, va);
     va_end(va);
 
-    Py_DECREF(callable);
     return retval;
 }
 
@@ -710,30 +756,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(tstate, 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 +782,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(tstate, 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 ca94a735db5b8f..7dc72d48274567 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
@@ -397,7 +397,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;
@@ -437,7 +436,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;

_______________________________________________
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