@@ -76,6 +76,40 @@ TypeWatcher<LoadMethodCache> lm_watcher;
7676TypeWatcher<LoadTypeMethodCache> ltm_watcher;
7777constexpr uintptr_t kKindMask = 0x07 ;
7878
79+ // Low-bit tag on a LoadMethodCache entry's cached value. When clear, the value
80+ // is an untagged PyObject* for a bound method -- the common, hot case -- and is
81+ // bound to the receiver as a method. When set, the value is *not* a bound
82+ // method and falls into one of two cases distinguished by the rest of the bits:
83+ // * value == kLoadMethodGetAttrSentinel (just the tag bit, no pointer): the
84+ // attribute is absent from the type and must be resolved via __getattr__ /
85+ // __getattribute__ dispatch.
86+ // * value > kLoadMethodGetAttrSentinel: a tagged PyObject* for a staticmethod
87+ // descriptor or class variable; untag it and return it as a plain attribute
88+ // (no self binding).
89+ constexpr uintptr_t kLoadMethodUnboundTag = 0x1 ;
90+ constexpr uintptr_t kLoadMethodGetAttrSentinel = kLoadMethodUnboundTag ;
91+
92+ uintptr_t tagLoadMethodValue (PyObject* value, bool is_bound_method) {
93+ uintptr_t bits = reinterpret_cast <uintptr_t >(value);
94+ JIT_DCHECK (
95+ (bits & kLoadMethodUnboundTag ) == 0 ,
96+ " PyObject* expected to be aligned, low bit is used as a tag" );
97+ if (value == nullptr ) {
98+ // The attribute is absent from the type: store the getattr/getattribute
99+ // sentinel (just the tag bit, no pointer).
100+ return kLoadMethodGetAttrSentinel ;
101+ }
102+ return is_bound_method ? bits : (bits | kLoadMethodUnboundTag );
103+ }
104+
105+ PyObject* loadMethodValuePtr (uintptr_t bits) {
106+ return reinterpret_cast <PyObject*>(bits & ~kLoadMethodUnboundTag );
107+ }
108+
109+ bool loadMethodValueIsUnbound (uintptr_t bits) {
110+ return (bits & kLoadMethodUnboundTag ) != 0 ;
111+ }
112+
79113// Sentinel PyTypeObject that must never escape into user code.
80114#pragma clang diagnostic push
81115#pragma clang diagnostic ignored "-Wmissing-field-initializers"
@@ -1333,7 +1367,7 @@ LoadMethodCache::~LoadMethodCache() {
13331367 if (entry.type != nullptr ) {
13341368 lm_watcher.unwatch (entry.type , this );
13351369 entry.type .reset ();
1336- entry.value . reset () ;
1370+ entry.value = 0 ;
13371371 }
13381372 }
13391373}
@@ -1406,12 +1440,21 @@ LoadMethodResult LoadMethodCache::lookup(
14061440 continue ;
14071441 }
14081442
1409- if (entry.value != nullptr ) {
1410- return {Py_NewRef (entry.value ), Py_NewRef (obj)};
1443+ uintptr_t value = entry.value ;
1444+ if (!loadMethodValueIsUnbound (value)) {
1445+ // Bound method (common case): the low bit is clear, so the value is an
1446+ // untagged PyObject* which is a method-like object.
1447+ return {Py_NewRef (reinterpret_cast <PyObject*>(value)), Py_NewRef (obj)};
1448+ } else if (value != kLoadMethodGetAttrSentinel ) {
1449+ // A tagged pointer (value > the tag bit): a staticmethod or class
1450+ // variable. Untag it and return it as a plain attribute without binding
1451+ // the receiver as self.
1452+ return {Py_None, Py_NewRef (loadMethodValuePtr (value))};
14111453 }
14121454
1413- // NULL sentinel. Two kinds of types cache a NULL entry (see
1414- // lookupSlowPath); the cache is invalidated if the type changes:
1455+ // getattr/getattribute sentinel (value == kLoadMethodGetAttrSentinel).
1456+ // Two kinds of types cache the sentinel (see lookupSlowPath); the cache
1457+ // is invalidated if the type changes:
14151458 //
14161459 // * A type whose __getattr__ hook wraps the generic getattr we
14171460 // replicate: the attribute is genuinely absent, so we skip straight
@@ -1436,7 +1479,7 @@ void LoadMethodCache::typeChanged(PyTypeObject* type) {
14361479 for (auto & entry : entries_) {
14371480 if (entry.type == type) {
14381481 entry.type .reset ();
1439- entry.value . reset () ;
1482+ entry.value = 0 ;
14401483 }
14411484 }
14421485}
@@ -1466,19 +1509,21 @@ LoadMethodResult __attribute__((noinline)) LoadMethodCache::lookupSlowPath(
14661509 PyObject **dictptr, *dict;
14671510 PyObject* attr;
14681511 bool is_method = false ;
1512+ bool is_static_method = false ;
14691513
14701514 // A type with a __getattr__ hook is cacheable as long as the hook wraps the
14711515 // generic getattr we replicate below: the type-dict lookup is authoritative,
1472- // and a genuine miss is forwarded to __getattr__ (and cached as NULL).
1516+ // and a genuine miss is forwarded to __getattr__ (and cached as the
1517+ // sentinel).
14731518 bool has_getattr_hook =
14741519 tp->tp_getattro == Ci_tp_getattr_hook && hookUsesGenericGetAttr (tp);
14751520
14761521 if (tp->tp_getattro != PyObject_GenericGetAttr && !has_getattr_hook) {
14771522 // The type has a custom lookup we can't replicate (a custom
14781523 // __getattribute__, or a metaclass instance whose __getattribute__ is
1479- // type_getattro). Cache a NULL sentinel so future lookups hit the cache and
1480- // dispatch straight through the type's own lookup (see lookup()), then
1481- // service this miss now.
1524+ // type_getattro). Cache the getattr sentinel so future lookups hit the
1525+ // cache and dispatch straight through the type's own lookup (see lookup()),
1526+ // then service this miss now.
14821527 fill (tp, nullptr , name, /* has_getattr_hook */ false );
14831528 PyObject* res = PyObject_GetAttr (obj, name);
14841529 if (res != nullptr ) {
@@ -1497,6 +1542,11 @@ LoadMethodResult __attribute__((noinline)) LoadMethodCache::lookupSlowPath(
14971542 if (PyFunction_Check (descr) || Py_TYPE (descr) == &PyMethodDescr_Type ||
14981543 PyType_HasFeature (Py_TYPE (descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
14991544 is_method = true ;
1545+ } else if (Py_TYPE (descr) == &PyStaticMethod_Type) {
1546+ // A staticmethod is a non-data descriptor; it can still be shadowed by an
1547+ // instance attribute, so defer caching until after the instance dict
1548+ // check below.
1549+ is_static_method = true ;
15001550 } else {
15011551 f = descr->ob_type ->tp_descr_get ;
15021552 if (f != nullptr && PyDescr_IsData (descr)) {
@@ -1530,6 +1580,22 @@ LoadMethodResult __attribute__((noinline)) LoadMethodCache::lookupSlowPath(
15301580 return {descr, obj};
15311581 }
15321582
1583+ if (is_static_method) {
1584+ // The staticmethod was found in the type dict and is not shadowed by an
1585+ // instance attribute. Unwrap it to the underlying callable, cache that
1586+ // callable, and return it as a plain attribute (static methods do not bind
1587+ // to the instance).
1588+ BorrowedRef<> callable = Ci_PyStaticMethod_GetFunc (descr);
1589+ fill (
1590+ tp,
1591+ callable,
1592+ name,
1593+ /* has_getattr_hook=*/ false ,
1594+ /* is_bound_method=*/ false );
1595+ Py_DECREF (descr);
1596+ return {Py_None, Py_NewRef (callable)};
1597+ }
1598+
15331599 if (f != nullptr ) {
15341600 maybeCollectCacheStats (
15351601 cache_stats_, tp, name, CacheMissReason::kUncategorized );
@@ -1539,15 +1605,18 @@ LoadMethodResult __attribute__((noinline)) LoadMethodCache::lookupSlowPath(
15391605 }
15401606
15411607 if (descr != nullptr ) {
1542- maybeCollectCacheStats (
1543- cache_stats_, tp, name, CacheMissReason::kUncategorized );
1608+ // A class variable / non-descriptor attribute found on the type dict and
1609+ // not shadowed by an instance attribute. Cache it and return it as a plain
1610+ // attribute (not bound to the receiver).
1611+ fill (
1612+ tp, descr, name, /* has_getattr_hook=*/ false , /* is_bound_method=*/ false );
15441613 return {Py_None, descr};
15451614 }
15461615
15471616 // The attribute is absent from both the type dict and the instance dict. If
1548- // the type has a __getattr__ hook, cache a NULL sentinel so future lookups
1549- // hit the cache and dispatch to __getattr__ directly (see lookup()), then
1550- // service this miss through __getattr__ now.
1617+ // the type has a __getattr__ hook, cache the getattr sentinel so future
1618+ // lookups hit the cache and dispatch to __getattr__ directly (see lookup()),
1619+ // then service this miss through __getattr__ now.
15511620 if (has_getattr_hook) {
15521621 fill (tp, nullptr , name, /* has_getattr_hook */ true );
15531622 PyObject* result = getAttrFallback (obj, name);
@@ -1565,18 +1634,20 @@ void LoadMethodCache::fill(
15651634 BorrowedRef<PyTypeObject> type,
15661635 BorrowedRef<> value,
15671636 BorrowedRef<> name,
1568- bool has_getattr_hook) {
1637+ bool has_getattr_hook,
1638+ bool is_bound_method) {
15691639 if (!Ci_Type_HasValidVersionTag (type)) {
15701640 // The type must have a valid version tag in order for us to be able to
15711641 // invalidate the cache when the type is modified. See the comment at
15721642 // the top of `PyType_Modified` for more details.
15731643 return ;
15741644 }
15751645
1576- // `value` may be NULL here, which is a sentinel meaning the cache can't
1577- // resolve the attribute itself (see lookupSlowPath). `has_getattr_hook`
1578- // records which kind of sentinel this is so lookup() can dispatch without
1579- // recomputing it.
1646+ // `value` may be NULL here, which is encoded as the getattr sentinel (value
1647+ // == kLoadMethodGetAttrSentinel) meaning "the attribute is absent from the
1648+ // type" (see tagLoadMethodValue). This happens for types with a __getattr__
1649+ // hook or an unreplicable lookup (see lookupSlowPath); lookup() turns such a
1650+ // hit into a __getattr__ / __getattribute__ dispatch.
15801651 for (auto & entry : entries_) {
15811652 if (entry.type == nullptr ) {
15821653 uint32_t keys_version = 0 ;
@@ -1586,7 +1657,7 @@ void LoadMethodCache::fill(
15861657
15871658 lm_watcher.watch (type, this );
15881659 entry.type = type;
1589- entry.value = value;
1660+ entry.value = tagLoadMethodValue ( value, is_bound_method) ;
15901661 entry.keys_version = keys_version;
15911662 entry.has_getattr_hook = has_getattr_hook;
15921663 return ;
0 commit comments