gh-134043: use stackrefs in vectorcalling methods (#134044)

Adds `_PyObject_GetMethodStackRef` which uses stackrefs and takes advantage of deferred reference counting in free-threading while calling method objects in vectorcall.
This commit is contained in:
Kumar Aditya 2025-05-27 22:28:27 +05:30 committed by GitHub
parent 3f9eb55e09
commit a380d57873
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 148 additions and 13 deletions

View File

@ -897,6 +897,9 @@ extern PyObject *_PyType_LookupRefAndVersion(PyTypeObject *, PyObject *,
extern unsigned int
_PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out);
extern int _PyObject_GetMethodStackRef(PyThreadState *ts, PyObject *obj,
PyObject *name, _PyStackRef *method);
// Cache the provided init method in the specialization cache of type if the
// provided type version matches the current version of the type.
//

View File

@ -834,12 +834,15 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
assert(PyVectorcall_NARGS(nargsf) >= 1);
PyThreadState *tstate = _PyThreadState_GET();
PyObject *callable = NULL;
_PyCStackRef method;
_PyThreadState_PushCStackRef(tstate, &method);
/* Use args[0] as "self" argument */
int unbound = _PyObject_GetMethod(args[0], name, &callable);
if (callable == NULL) {
int unbound = _PyObject_GetMethodStackRef(tstate, args[0], name, &method.ref);
if (PyStackRef_IsNull(method.ref)) {
_PyThreadState_PopCStackRef(tstate, &method);
return NULL;
}
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
if (unbound) {
/* We must remove PY_VECTORCALL_ARGUMENTS_OFFSET since
@ -855,7 +858,7 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_METHOD, callable);
PyObject *result = _PyObject_VectorcallTstate(tstate, callable,
args, nargsf, kwnames);
Py_DECREF(callable);
_PyThreadState_PopCStackRef(tstate, &method);
return result;
}
@ -868,11 +871,14 @@ PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...)
return null_error(tstate);
}
PyObject *callable = NULL;
int is_method = _PyObject_GetMethod(obj, name, &callable);
if (callable == NULL) {
_PyCStackRef method;
_PyThreadState_PushCStackRef(tstate, &method);
int is_method = _PyObject_GetMethodStackRef(tstate, obj, name, &method.ref);
if (PyStackRef_IsNull(method.ref)) {
_PyThreadState_PopCStackRef(tstate, &method);
return NULL;
}
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
obj = is_method ? obj : NULL;
va_list vargs;
@ -880,7 +886,7 @@ PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...)
PyObject *result = object_vacall(tstate, obj, callable, vargs);
va_end(vargs);
Py_DECREF(callable);
_PyThreadState_PopCStackRef(tstate, &method);
return result;
}
@ -897,12 +903,15 @@ _PyObject_CallMethodIdObjArgs(PyObject *obj, _Py_Identifier *name, ...)
if (!oname) {
return NULL;
}
PyObject *callable = NULL;
int is_method = _PyObject_GetMethod(obj, oname, &callable);
if (callable == NULL) {
_PyCStackRef method;
_PyThreadState_PushCStackRef(tstate, &method);
int is_method = _PyObject_GetMethodStackRef(tstate, obj, oname, &method.ref);
if (PyStackRef_IsNull(method.ref)) {
_PyThreadState_PopCStackRef(tstate, &method);
return NULL;
}
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
obj = is_method ? obj : NULL;
va_list vargs;
@ -910,7 +919,7 @@ _PyObject_CallMethodIdObjArgs(PyObject *obj, _Py_Identifier *name, ...)
PyObject *result = object_vacall(tstate, obj, callable, vargs);
va_end(vargs);
Py_DECREF(callable);
_PyThreadState_PopCStackRef(tstate, &method);
return result;
}

View File

@ -1664,6 +1664,116 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
return 0;
}
int
_PyObject_GetMethodStackRef(PyThreadState *ts, PyObject *obj,
PyObject *name, _PyStackRef *method)
{
int meth_found = 0;
assert(PyStackRef_IsNull(*method));
PyTypeObject *tp = Py_TYPE(obj);
if (!_PyType_IsReady(tp)) {
if (PyType_Ready(tp) < 0) {
return 0;
}
}
if (tp->tp_getattro != PyObject_GenericGetAttr || !PyUnicode_CheckExact(name)) {
PyObject *res = PyObject_GetAttr(obj, name);
if (res != NULL) {
*method = PyStackRef_FromPyObjectSteal(res);
}
return 0;
}
_PyType_LookupStackRefAndVersion(tp, name, method);
PyObject *descr = PyStackRef_AsPyObjectBorrow(*method);
descrgetfunc f = NULL;
if (descr != NULL) {
if (_PyType_HasFeature(Py_TYPE(descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
meth_found = 1;
}
else {
f = Py_TYPE(descr)->tp_descr_get;
if (f != NULL && PyDescr_IsData(descr)) {
PyObject *value = f(descr, obj, (PyObject *)Py_TYPE(obj));
PyStackRef_CLEAR(*method);
if (value != NULL) {
*method = PyStackRef_FromPyObjectSteal(value);
}
return 0;
}
}
}
PyObject *dict, *attr;
if ((tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) &&
_PyObject_TryGetInstanceAttribute(obj, name, &attr)) {
if (attr != NULL) {
PyStackRef_CLEAR(*method);
*method = PyStackRef_FromPyObjectSteal(attr);
return 0;
}
dict = NULL;
}
else if ((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT)) {
dict = (PyObject *)_PyObject_GetManagedDict(obj);
}
else {
PyObject **dictptr = _PyObject_ComputedDictPointer(obj);
if (dictptr != NULL) {
dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*dictptr);
}
else {
dict = NULL;
}
}
if (dict != NULL) {
// TODO: use _Py_dict_lookup_threadsafe_stackref
Py_INCREF(dict);
PyObject *value;
if (PyDict_GetItemRef(dict, name, &value) != 0) {
// found or error
Py_DECREF(dict);
PyStackRef_CLEAR(*method);
if (value != NULL) {
*method = PyStackRef_FromPyObjectSteal(value);
}
return 0;
}
// not found
Py_DECREF(dict);
}
if (meth_found) {
assert(!PyStackRef_IsNull(*method));
return 1;
}
if (f != NULL) {
PyObject *value = f(descr, obj, (PyObject *)Py_TYPE(obj));
PyStackRef_CLEAR(*method);
if (value) {
*method = PyStackRef_FromPyObjectSteal(value);
}
return 0;
}
if (descr != NULL) {
assert(!PyStackRef_IsNull(*method));
return 0;
}
PyErr_Format(PyExc_AttributeError,
"'%.100s' object has no attribute '%U'",
tp->tp_name, name);
_PyObject_SetAttributeErrorContext(obj, name);
assert(PyStackRef_IsNull(*method));
return 0;
}
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot. */
PyObject *

View File

@ -27,6 +27,7 @@ import queue
import sys
import threading
import time
from operator import methodcaller
# The iterations in individual benchmarks are scaled by this factor.
WORK_SCALE = 100
@ -188,6 +189,18 @@ def thread_local_read():
_ = tmp.x
_ = tmp.x
class MyClass:
__slots__ = ()
def func(self):
pass
@register_benchmark
def method_caller():
mc = methodcaller("func")
obj = MyClass()
for i in range(1000 * WORK_SCALE):
mc(obj)
def bench_one_thread(func):
t0 = time.perf_counter_ns()