Skip to content

Commit a380d57

Browse files
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.
1 parent 3f9eb55 commit a380d57

File tree

4 files changed

+148
-13
lines changed

4 files changed

+148
-13
lines changed

Include/internal/pycore_object.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,9 @@ extern PyObject *_PyType_LookupRefAndVersion(PyTypeObject *, PyObject *,
897897
extern unsigned int
898898
_PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out);
899899

900+
extern int _PyObject_GetMethodStackRef(PyThreadState *ts, PyObject *obj,
901+
PyObject *name, _PyStackRef *method);
902+
900903
// Cache the provided init method in the specialization cache of type if the
901904
// provided type version matches the current version of the type.
902905
//

Objects/call.c

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -834,12 +834,15 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
834834
assert(PyVectorcall_NARGS(nargsf) >= 1);
835835

836836
PyThreadState *tstate = _PyThreadState_GET();
837-
PyObject *callable = NULL;
837+
_PyCStackRef method;
838+
_PyThreadState_PushCStackRef(tstate, &method);
838839
/* Use args[0] as "self" argument */
839-
int unbound = _PyObject_GetMethod(args[0], name, &callable);
840-
if (callable == NULL) {
840+
int unbound = _PyObject_GetMethodStackRef(tstate, args[0], name, &method.ref);
841+
if (PyStackRef_IsNull(method.ref)) {
842+
_PyThreadState_PopCStackRef(tstate, &method);
841843
return NULL;
842844
}
845+
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
843846

844847
if (unbound) {
845848
/* We must remove PY_VECTORCALL_ARGUMENTS_OFFSET since
@@ -855,7 +858,7 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
855858
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_METHOD, callable);
856859
PyObject *result = _PyObject_VectorcallTstate(tstate, callable,
857860
args, nargsf, kwnames);
858-
Py_DECREF(callable);
861+
_PyThreadState_PopCStackRef(tstate, &method);
859862
return result;
860863
}
861864

@@ -868,19 +871,22 @@ PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...)
868871
return null_error(tstate);
869872
}
870873

871-
PyObject *callable = NULL;
872-
int is_method = _PyObject_GetMethod(obj, name, &callable);
873-
if (callable == NULL) {
874+
_PyCStackRef method;
875+
_PyThreadState_PushCStackRef(tstate, &method);
876+
int is_method = _PyObject_GetMethodStackRef(tstate, obj, name, &method.ref);
877+
if (PyStackRef_IsNull(method.ref)) {
878+
_PyThreadState_PopCStackRef(tstate, &method);
874879
return NULL;
875880
}
881+
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
876882
obj = is_method ? obj : NULL;
877883

878884
va_list vargs;
879885
va_start(vargs, name);
880886
PyObject *result = object_vacall(tstate, obj, callable, vargs);
881887
va_end(vargs);
882888

883-
Py_DECREF(callable);
889+
_PyThreadState_PopCStackRef(tstate, &method);
884890
return result;
885891
}
886892

@@ -897,20 +903,23 @@ _PyObject_CallMethodIdObjArgs(PyObject *obj, _Py_Identifier *name, ...)
897903
if (!oname) {
898904
return NULL;
899905
}
900-
901-
PyObject *callable = NULL;
902-
int is_method = _PyObject_GetMethod(obj, oname, &callable);
903-
if (callable == NULL) {
906+
_PyCStackRef method;
907+
_PyThreadState_PushCStackRef(tstate, &method);
908+
int is_method = _PyObject_GetMethodStackRef(tstate, obj, oname, &method.ref);
909+
if (PyStackRef_IsNull(method.ref)) {
910+
_PyThreadState_PopCStackRef(tstate, &method);
904911
return NULL;
905912
}
913+
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
914+
906915
obj = is_method ? obj : NULL;
907916

908917
va_list vargs;
909918
va_start(vargs, name);
910919
PyObject *result = object_vacall(tstate, obj, callable, vargs);
911920
va_end(vargs);
912921

913-
Py_DECREF(callable);
922+
_PyThreadState_PopCStackRef(tstate, &method);
914923
return result;
915924
}
916925

Objects/object.c

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,6 +1664,116 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
16641664
return 0;
16651665
}
16661666

1667+
int
1668+
_PyObject_GetMethodStackRef(PyThreadState *ts, PyObject *obj,
1669+
PyObject *name, _PyStackRef *method)
1670+
{
1671+
int meth_found = 0;
1672+
1673+
assert(PyStackRef_IsNull(*method));
1674+
1675+
PyTypeObject *tp = Py_TYPE(obj);
1676+
if (!_PyType_IsReady(tp)) {
1677+
if (PyType_Ready(tp) < 0) {
1678+
return 0;
1679+
}
1680+
}
1681+
1682+
if (tp->tp_getattro != PyObject_GenericGetAttr || !PyUnicode_CheckExact(name)) {
1683+
PyObject *res = PyObject_GetAttr(obj, name);
1684+
if (res != NULL) {
1685+
*method = PyStackRef_FromPyObjectSteal(res);
1686+
}
1687+
return 0;
1688+
}
1689+
1690+
_PyType_LookupStackRefAndVersion(tp, name, method);
1691+
PyObject *descr = PyStackRef_AsPyObjectBorrow(*method);
1692+
descrgetfunc f = NULL;
1693+
if (descr != NULL) {
1694+
if (_PyType_HasFeature(Py_TYPE(descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
1695+
meth_found = 1;
1696+
}
1697+
else {
1698+
f = Py_TYPE(descr)->tp_descr_get;
1699+
if (f != NULL && PyDescr_IsData(descr)) {
1700+
PyObject *value = f(descr, obj, (PyObject *)Py_TYPE(obj));
1701+
PyStackRef_CLEAR(*method);
1702+
if (value != NULL) {
1703+
*method = PyStackRef_FromPyObjectSteal(value);
1704+
}
1705+
return 0;
1706+
}
1707+
}
1708+
}
1709+
PyObject *dict, *attr;
1710+
if ((tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) &&
1711+
_PyObject_TryGetInstanceAttribute(obj, name, &attr)) {
1712+
if (attr != NULL) {
1713+
PyStackRef_CLEAR(*method);
1714+
*method = PyStackRef_FromPyObjectSteal(attr);
1715+
return 0;
1716+
}
1717+
dict = NULL;
1718+
}
1719+
else if ((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT)) {
1720+
dict = (PyObject *)_PyObject_GetManagedDict(obj);
1721+
}
1722+
else {
1723+
PyObject **dictptr = _PyObject_ComputedDictPointer(obj);
1724+
if (dictptr != NULL) {
1725+
dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*dictptr);
1726+
}
1727+
else {
1728+
dict = NULL;
1729+
}
1730+
}
1731+
if (dict != NULL) {
1732+
// TODO: use _Py_dict_lookup_threadsafe_stackref
1733+
Py_INCREF(dict);
1734+
PyObject *value;
1735+
if (PyDict_GetItemRef(dict, name, &value) != 0) {
1736+
// found or error
1737+
Py_DECREF(dict);
1738+
PyStackRef_CLEAR(*method);
1739+
if (value != NULL) {
1740+
*method = PyStackRef_FromPyObjectSteal(value);
1741+
}
1742+
return 0;
1743+
}
1744+
// not found
1745+
Py_DECREF(dict);
1746+
}
1747+
1748+
if (meth_found) {
1749+
assert(!PyStackRef_IsNull(*method));
1750+
return 1;
1751+
}
1752+
1753+
if (f != NULL) {
1754+
PyObject *value = f(descr, obj, (PyObject *)Py_TYPE(obj));
1755+
PyStackRef_CLEAR(*method);
1756+
if (value) {
1757+
*method = PyStackRef_FromPyObjectSteal(value);
1758+
}
1759+
return 0;
1760+
}
1761+
1762+
if (descr != NULL) {
1763+
assert(!PyStackRef_IsNull(*method));
1764+
return 0;
1765+
}
1766+
1767+
PyErr_Format(PyExc_AttributeError,
1768+
"'%.100s' object has no attribute '%U'",
1769+
tp->tp_name, name);
1770+
1771+
_PyObject_SetAttributeErrorContext(obj, name);
1772+
assert(PyStackRef_IsNull(*method));
1773+
return 0;
1774+
}
1775+
1776+
16671777
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot. */
16681778

16691779
PyObject *

Tools/ftscalingbench/ftscalingbench.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import sys
2828
import threading
2929
import time
30+
from operator import methodcaller
3031

3132
# The iterations in individual benchmarks are scaled by this factor.
3233
WORK_SCALE = 100
@@ -188,6 +189,18 @@ def thread_local_read():
188189
_ = tmp.x
189190
_ = tmp.x
190191

192+
class MyClass:
193+
__slots__ = ()
194+
195+
def func(self):
196+
pass
197+
198+
@register_benchmark
199+
def method_caller():
200+
mc = methodcaller("func")
201+
obj = MyClass()
202+
for i in range(1000 * WORK_SCALE):
203+
mc(obj)
191204

192205
def bench_one_thread(func):
193206
t0 = time.perf_counter_ns()

0 commit comments

Comments
 (0)