Files
3f9118f1d1 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) <noreply@anthropic.com>

* 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>

* rename to callmethod

* review comments

* review comments

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Co-authored-by: Inada Naoki <songofacandy@gmail.com>
2026-10-05 18:44:28 +00:00

182 lines
5.4 KiB
C

#ifndef Py_INTERNAL_CALL_H
#define Py_INTERNAL_CALL_H
#ifdef __cplusplus
extern "C" {
#endif
#ifndef Py_BUILD_CORE
# error "this header requires Py_BUILD_CORE define"
#endif
#include "pycore_code.h" // EVAL_CALL_STAT_INC_IF_FUNCTION()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_stats.h"
/* Flags that determine the C calling convention. */
#define _Py_METH_CALL_FLAGS \
(METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | \
METH_KEYWORDS | METH_METHOD)
/* Suggested size (number of positional arguments) for arrays of PyObject*
allocated on a C stack to avoid allocating memory on the heap memory. Such
array is used to pass positional arguments to call functions of the
PyObject_Vectorcall() family.
The size is chosen to not abuse the C stack and so limit the risk of stack
overflow. The size is also chosen to allow using the small stack for most
function calls of the Python standard library. On 64-bit CPU, it allocates
40 bytes on the stack. */
#define _PY_FASTCALL_SMALL_STACK 5
// Export for 'math' shared extension, used via _PyObject_VectorcallTstate()
// static inline function.
PyAPI_FUNC(PyObject*) _Py_CheckFunctionResult(
PyThreadState *tstate,
PyObject *callable,
PyObject *result,
const char *where);
extern PyObject* _PyObject_Call_Prepend(
PyThreadState *tstate,
PyObject *callable,
PyObject *obj,
PyObject *args,
PyObject *kwargs);
extern PyObject* _PyObject_VectorcallDictTstate(
PyThreadState *tstate,
PyObject *callable,
PyObject *const *args,
size_t nargsf,
PyObject *kwargs);
extern PyObject* _PyObject_Call(
PyThreadState *tstate,
PyObject *callable,
PyObject *args,
PyObject *kwargs);
// Export for 'array' shared extension
PyAPI_FUNC(PyObject*) _PyObject_CallMethod(
PyObject *obj,
PyObject *name,
const char *format, ...);
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(),
// except that nargs is plainly the number of arguments without flags.
//
// Export for 'math' shared extension, used via _PyObject_VectorcallTstate()
// static inline function.
PyAPI_FUNC(PyObject*) _PyObject_MakeTpCall(
PyThreadState *tstate,
PyObject *callable,
PyObject *const *args, Py_ssize_t nargs,
PyObject *keywords);
// Static inline variant of public PyVectorcall_Function().
static inline vectorcallfunc
_PyVectorcall_FunctionInline(PyObject *callable)
{
assert(callable != NULL);
PyTypeObject *tp = Py_TYPE(callable);
if (!PyType_HasFeature(tp, Py_TPFLAGS_HAVE_VECTORCALL)) {
return NULL;
}
assert(PyCallable_Check(callable));
Py_ssize_t offset = tp->tp_vectorcall_offset;
assert(offset > 0);
vectorcallfunc ptr;
memcpy(&ptr, (char *) callable + offset, sizeof(ptr));
return ptr;
}
/* Call the callable object 'callable' with the "vectorcall" calling
convention.
args is a C array for positional arguments.
nargsf is the number of positional arguments plus optionally the flag
PY_VECTORCALL_ARGUMENTS_OFFSET which means that the caller is allowed to
modify args[-1].
kwnames is a tuple of keyword names. The values of the keyword arguments
are stored in "args" after the positional arguments (note that the number
of keyword arguments does not change nargsf). kwnames can also be NULL if
there are no keyword arguments.
keywords must only contain strings and all keys must be unique.
Return the result on success. Raise an exception and return NULL on
error. */
static inline PyObject *
_PyObject_VectorcallTstate(PyThreadState *tstate, PyObject *callable,
PyObject *const *args, size_t nargsf,
PyObject *kwnames)
{
vectorcallfunc func;
PyObject *res;
assert(kwnames == NULL || PyTuple_Check(kwnames));
assert(args != NULL || PyVectorcall_NARGS(nargsf) == 0);
func = _PyVectorcall_FunctionInline(callable);
if (func == NULL) {
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
return _PyObject_MakeTpCall(tstate, callable, args, nargs, kwnames);
}
res = func(callable, args, nargsf, kwnames);
return _Py_CheckFunctionResult(tstate, callable, res, NULL);
}
static inline PyObject *
_PyObject_CallNoArgsTstate(PyThreadState *tstate, PyObject *func) {
return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL);
}
// Private static inline function variant of public PyObject_CallNoArgs()
static inline PyObject *
_PyObject_CallNoArgs(PyObject *func) {
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func);
PyThreadState *tstate = _PyThreadState_GET();
return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL);
}
PyAPI_FUNC(PyObject *const *)
_PyStack_UnpackDict(PyThreadState *tstate,
PyObject *const *args, Py_ssize_t nargs,
PyObject *kwargs, PyObject **p_kwnames);
// Exported for external JIT support
PyAPI_FUNC(void) _PyStack_UnpackDict_Free(
PyObject *const *stack,
Py_ssize_t nargs,
PyObject *kwnames);
PyAPI_FUNC(void) _PyStack_UnpackDict_FreeNoDecRef(
PyObject *const *stack,
PyObject *kwnames);
#ifdef __cplusplus
}
#endif
#endif /* !Py_INTERNAL_CALL_H */