Skip to content

Commit be5fa22

Browse files
Make LOAD_ATTR deferred
1 parent 401fff7 commit be5fa22

File tree

9 files changed

+290
-34
lines changed

9 files changed

+290
-34
lines changed

Include/internal/pycore_dict.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ PyAPI_FUNC(int) _PyDict_SetItem_KnownHash_LockHeld(PyDictObject *mp, PyObject *k
118118
PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash_LockHeld(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result);
119119
extern int _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result);
120120
extern int _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **result);
121+
extern int _PyDict_GetItem_KnownHash_StackRef(PyDictObject *op, PyObject *key, Py_hash_t hash, _PyStackRef *result);
121122
extern int _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr, PyObject *name, PyObject *value);
122123

123124
extern int _PyDict_Pop_KnownHash(

Include/internal/pycore_object.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,7 @@ PyAPI_FUNC(PyObject*) _PyObject_LookupSpecialMethod(PyObject *self, PyObject *at
837837
extern int _PyObject_IsAbstract(PyObject *);
838838

839839
PyAPI_FUNC(int) _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method);
840+
PyAPI_FUNC(int) _PyObject_GetMethodStackRef(PyObject *obj, PyObject *name, _PyStackRef *method);
840841
extern PyObject* _PyObject_NextNotImplemented(PyObject *);
841842

842843
// Pickle support.
@@ -874,6 +875,8 @@ PyAPI_DATA(int) _Py_SwappedOp[];
874875

875876
extern void _Py_GetConstant_Init(void);
876877

878+
extern void _PyType_LookupStackRef(PyTypeObject *type, PyObject *name, _PyStackRef *result);
879+
877880
#ifdef __cplusplus
878881
}
879882
#endif

Include/internal/pycore_stackref.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,7 @@ PyStackRef_FromPyObjectNew(PyObject *obj)
149149
{
150150
// Make sure we don't take an already tagged value.
151151
assert(((uintptr_t)obj & Py_TAG_BITS) == 0);
152-
assert(obj != NULL);
153-
if (_Py_IsImmortal(obj) || _PyObject_HasDeferredRefcount(obj)) {
152+
if (obj == NULL || _Py_IsImmortal(obj) || _PyObject_HasDeferredRefcount(obj)) {
154153
return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_DEFERRED };
155154
}
156155
else {

Objects/dictobject.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2324,6 +2324,27 @@ _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, Py
23242324
return 1; // key is present
23252325
}
23262326

2327+
int
2328+
_PyDict_GetItem_KnownHash_StackRef(PyDictObject *op, PyObject *key, Py_hash_t hash, _PyStackRef *result)
2329+
{
2330+
#ifdef Py_GIL_DISABLED
2331+
Py_ssize_t ix = _Py_dict_lookup_threadsafe_stackref(op, key, hash, result);
2332+
#else
2333+
PyObject *value;
2334+
Py_ssize_t ix = _Py_dict_lookup(op, key, hash, &value);
2335+
*result = PyStackRef_FromPyObjectSteal(value);
2336+
#endif
2337+
assert(ix >= 0 || PyStackRef_IsNull(*result));
2338+
if (ix == DKIX_ERROR) {
2339+
*result = PyStackRef_NULL;
2340+
return -1;
2341+
}
2342+
if (PyStackRef_IsNull(*result)) {
2343+
return 0; // missing key
2344+
}
2345+
return 1; // key is present
2346+
}
2347+
23272348
int
23282349
PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result)
23292350
{

Objects/object.c

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,6 +1581,106 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
15811581
return 0;
15821582
}
15831583

1584+
int
1585+
_PyObject_GetMethodStackRef(PyObject *obj, PyObject *name, _PyStackRef *method)
1586+
{
1587+
1588+
int meth_found = 0;
1589+
1590+
assert(PyStackRef_IsNull(*method));
1591+
1592+
PyTypeObject *tp = Py_TYPE(obj);
1593+
if (!_PyType_IsReady(tp)) {
1594+
if (PyType_Ready(tp) < 0) {
1595+
return 0;
1596+
}
1597+
}
1598+
1599+
if (tp->tp_getattro != PyObject_GenericGetAttr || !PyUnicode_CheckExact(name)) {
1600+
*method = PyStackRef_FromPyObjectSteal(PyObject_GetAttr(obj, name));
1601+
return 0;
1602+
}
1603+
1604+
// Directly set it to that if a GC cycle happens, the descriptor doesn't get
1605+
// evaporated.
1606+
// This is why we no longer need a strong reference for this if it's
1607+
// deferred.
1608+
_PyType_LookupStackRef(tp, name, method);
1609+
_PyStackRef descr_st = *method;
1610+
descrgetfunc f = NULL;
1611+
if (!PyStackRef_IsNull(descr_st)) {
1612+
if (_PyType_HasFeature(PyStackRef_TYPE(descr_st), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
1613+
meth_found = 1;
1614+
} else {
1615+
f = PyStackRef_TYPE(descr_st)->tp_descr_get;
1616+
if (f != NULL && PyDescr_IsData(PyStackRef_AsPyObjectBorrow(descr_st))) {
1617+
*method = PyStackRef_FromPyObjectSteal(f(PyStackRef_AsPyObjectBorrow(descr_st), obj, (PyObject *)Py_TYPE(obj)));
1618+
PyStackRef_CLOSE(descr_st);
1619+
return 0;
1620+
}
1621+
}
1622+
}
1623+
PyObject *dict, *attr;
1624+
if ((tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) &&
1625+
_PyObject_TryGetInstanceAttribute(obj, name, &attr)) {
1626+
if (attr != NULL) {
1627+
*method = PyStackRef_FromPyObjectSteal(attr);
1628+
PyStackRef_XCLOSE(descr_st);
1629+
return 0;
1630+
}
1631+
dict = NULL;
1632+
}
1633+
else if ((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT)) {
1634+
dict = (PyObject *)_PyObject_GetManagedDict(obj);
1635+
}
1636+
else {
1637+
PyObject **dictptr = _PyObject_ComputedDictPointer(obj);
1638+
if (dictptr != NULL) {
1639+
dict = *dictptr;
1640+
}
1641+
else {
1642+
dict = NULL;
1643+
}
1644+
}
1645+
if (dict != NULL) {
1646+
Py_INCREF(dict);
1647+
PyObject *item;
1648+
if (PyDict_GetItemRef(dict, name, &item) != 0) {
1649+
*method = PyStackRef_FromPyObjectSteal(item);
1650+
// found or error
1651+
Py_DECREF(dict);
1652+
PyStackRef_CLOSE(descr_st);
1653+
return 0;
1654+
}
1655+
// not found
1656+
Py_DECREF(dict);
1657+
}
1658+
1659+
if (meth_found) {
1660+
*method = descr_st;
1661+
return 1;
1662+
}
1663+
1664+
if (f != NULL) {
1665+
*method = PyStackRef_FromPyObjectSteal(f(PyStackRef_AsPyObjectBorrow(descr_st), obj, (PyObject *)Py_TYPE(obj)));
1666+
PyStackRef_CLOSE(descr_st);
1667+
return 0;
1668+
}
1669+
1670+
if (!PyStackRef_IsNull(descr_st)) {
1671+
*method = descr_st;
1672+
return 0;
1673+
}
1674+
1675+
*method = PyStackRef_NULL;
1676+
PyErr_Format(PyExc_AttributeError,
1677+
"'%.100s' object has no attribute '%U'",
1678+
tp->tp_name, name);
1679+
1680+
_PyObject_SetAttributeErrorContext(obj, name);
1681+
return 0;
1682+
}
1683+
15841684
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot. */
15851685

15861686
PyObject *

Objects/typeobject.c

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5356,6 +5356,58 @@ find_name_in_mro(PyTypeObject *type, PyObject *name, int *error)
53565356
return res;
53575357
}
53585358

5359+
5360+
static void
5361+
find_name_in_mro_stackref(PyTypeObject *type, PyObject *name, int *error, _PyStackRef *result)
5362+
{
5363+
ASSERT_TYPE_LOCK_HELD();
5364+
5365+
Py_hash_t hash = _PyObject_HashFast(name);
5366+
if (hash == -1) {
5367+
*error = -1;
5368+
*result = PyStackRef_NULL;
5369+
return;
5370+
}
5371+
5372+
/* Look in tp_dict of types in MRO */
5373+
PyObject *mro = lookup_tp_mro(type);
5374+
if (mro == NULL) {
5375+
if (!is_readying(type)) {
5376+
if (PyType_Ready(type) < 0) {
5377+
*error = -1;
5378+
*result = PyStackRef_NULL;
5379+
return;
5380+
}
5381+
mro = lookup_tp_mro(type);
5382+
}
5383+
if (mro == NULL) {
5384+
*error = 1;
5385+
*result = PyStackRef_NULL;
5386+
return;
5387+
}
5388+
}
5389+
5390+
/* Keep a strong reference to mro because type->tp_mro can be replaced
5391+
during dict lookup, e.g. when comparing to non-string keys. */
5392+
Py_INCREF(mro);
5393+
Py_ssize_t n = PyTuple_GET_SIZE(mro);
5394+
for (Py_ssize_t i = 0; i < n; i++) {
5395+
PyObject *base = PyTuple_GET_ITEM(mro, i);
5396+
PyObject *dict = lookup_tp_dict(_PyType_CAST(base));
5397+
assert(dict && PyDict_Check(dict));
5398+
if (_PyDict_GetItem_KnownHash_StackRef((PyDictObject *)dict, name, hash, result) < 0) {
5399+
*error = -1;
5400+
goto done;
5401+
}
5402+
if (!PyStackRef_IsNull(*result)) {
5403+
break;
5404+
}
5405+
}
5406+
*error = 0;
5407+
done:
5408+
Py_DECREF(mro);
5409+
}
5410+
53595411
/* Check if the "readied" PyUnicode name
53605412
is a double-underscore special name. */
53615413
static int
@@ -5528,6 +5580,95 @@ _PyType_LookupRef(PyTypeObject *type, PyObject *name)
55285580
return res;
55295581
}
55305582

5583+
void
5584+
_PyType_LookupStackRef(PyTypeObject *type, PyObject *name, _PyStackRef *result)
5585+
{
5586+
int error;
5587+
PyInterpreterState *interp = _PyInterpreterState_GET();
5588+
5589+
unsigned int h = MCACHE_HASH_METHOD(type, name);
5590+
struct type_cache *cache = get_type_cache();
5591+
struct type_cache_entry *entry = &cache->hashtable[h];
5592+
#ifdef Py_GIL_DISABLED
5593+
// synchronize-with other writing threads by doing an acquire load on the sequence
5594+
while (1) {
5595+
int sequence = _PySeqLock_BeginRead(&entry->sequence);
5596+
uint32_t entry_version = _Py_atomic_load_uint32_relaxed(&entry->version);
5597+
uint32_t type_version = _Py_atomic_load_uint32_acquire(&type->tp_version_tag);
5598+
if (entry_version == type_version &&
5599+
_Py_atomic_load_ptr_relaxed(&entry->name) == name) {
5600+
OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name));
5601+
OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name));
5602+
PyObject *value = _Py_atomic_load_ptr_relaxed(&entry->value);
5603+
*result = PyStackRef_FromPyObjectNew(value);
5604+
// If the sequence is still valid then we're done
5605+
if (_PySeqLock_EndRead(&entry->sequence, sequence)) {
5606+
return;
5607+
}
5608+
*result = PyStackRef_NULL;
5609+
}
5610+
else {
5611+
// cache miss
5612+
break;
5613+
}
5614+
}
5615+
#else
5616+
if (entry->version == type->tp_version_tag &&
5617+
entry->name == name) {
5618+
assert(type->tp_version_tag);
5619+
OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name));
5620+
OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name));
5621+
Py_XINCREF(entry->value);
5622+
return entry->value;
5623+
}
5624+
#endif
5625+
OBJECT_STAT_INC_COND(type_cache_misses, !is_dunder_name(name));
5626+
OBJECT_STAT_INC_COND(type_cache_dunder_misses, is_dunder_name(name));
5627+
5628+
/* We may end up clearing live exceptions below, so make sure it's ours. */
5629+
assert(!PyErr_Occurred());
5630+
5631+
// We need to atomically do the lookup and capture the version before
5632+
// anyone else can modify our mro or mutate the type.
5633+
5634+
int has_version = 0;
5635+
int version = 0;
5636+
BEGIN_TYPE_LOCK();
5637+
find_name_in_mro_stackref(type, name, &error, result);
5638+
if (MCACHE_CACHEABLE_NAME(name)) {
5639+
has_version = assign_version_tag(interp, type);
5640+
version = type->tp_version_tag;
5641+
}
5642+
END_TYPE_LOCK();
5643+
5644+
/* Only put NULL results into cache if there was no error. */
5645+
if (error) {
5646+
/* It's not ideal to clear the error condition,
5647+
but this function is documented as not setting
5648+
an exception, and I don't want to change that.
5649+
E.g., when PyType_Ready() can't proceed, it won't
5650+
set the "ready" flag, so future attempts to ready
5651+
the same type will call it again -- hopefully
5652+
in a context that propagates the exception out.
5653+
*/
5654+
if (error == -1) {
5655+
PyErr_Clear();
5656+
}
5657+
*result = PyStackRef_NULL;
5658+
return;
5659+
}
5660+
5661+
if (has_version) {
5662+
#if Py_GIL_DISABLED
5663+
update_cache_gil_disabled(entry, name, version,
5664+
PyStackRef_AsPyObjectBorrow(*result));
5665+
#else
5666+
PyObject *old_value = update_cache(entry, name, version, PyStackRef_AsPyObjectBorrow(*result));
5667+
Py_DECREF(old_value);
5668+
#endif
5669+
}
5670+
}
5671+
55315672
PyObject *
55325673
_PyType_Lookup(PyTypeObject *type, PyObject *name)
55335674
{

Python/bytecodes.c

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,19 +1977,17 @@ dummy_func(
19771977
#endif /* ENABLE_SPECIALIZATION */
19781978
}
19791979

1980-
op(_LOAD_ATTR, (owner -- attr, self_or_null if (oparg & 1))) {
1980+
op(_LOAD_ATTR, (owner -- attr: _PyStackRef *, self_or_null if (oparg & 1))) {
19811981
PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1);
1982-
PyObject *attr_o;
19831982
if (oparg & 1) {
19841983
/* Designed to work in tandem with CALL, pushes two values. */
1985-
attr_o = NULL;
1986-
int is_meth = _PyObject_GetMethod(PyStackRef_AsPyObjectBorrow(owner), name, &attr_o);
1987-
if (is_meth) {
1984+
*attr = PyStackRef_NULL;
1985+
if (_PyObject_GetMethodStackRef(PyStackRef_AsPyObjectBorrow(owner), name, attr)) {
19881986
/* We can bypass temporary bound method object.
19891987
meth is unbound method and obj is self.
19901988
meth | self | arg1 | ... | argN
19911989
*/
1992-
assert(attr_o != NULL); // No errors on this branch
1990+
assert(!PyStackRef_IsNull(*attr)); // No errors on this branch
19931991
self_or_null = owner; // Transfer ownership
19941992
}
19951993
else {
@@ -2000,17 +1998,16 @@ dummy_func(
20001998
meth | NULL | arg1 | ... | argN
20011999
*/
20022000
DECREF_INPUTS();
2003-
ERROR_IF(attr_o == NULL, error);
2001+
ERROR_IF(PyStackRef_IsNull(*attr), error);
20042002
self_or_null = PyStackRef_NULL;
20052003
}
20062004
}
20072005
else {
20082006
/* Classic, pushes one value. */
2009-
attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name);
2007+
*attr = PyStackRef_FromPyObjectSteal(PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name));
20102008
DECREF_INPUTS();
2011-
ERROR_IF(attr_o == NULL, error);
2009+
ERROR_IF(PyStackRef_IsNull(*attr), error);
20122010
}
2013-
attr = PyStackRef_FromPyObjectSteal(attr_o);
20142011
}
20152012

20162013
macro(LOAD_ATTR) =

0 commit comments

Comments
 (0)