Skip to content

Commit 183cbf7

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Add caching for unbound objects
Summary: This adds support for caching non-method objects too. These can show up as static methods, types, etc... which are all things which people invoke. Unlike D109636527 we use a tag on the value to avoid having to introduce an extra load to see if we have a non-method object. Reviewed By: alexmalyshev Differential Revision: D109720015 fbshipit-source-id: eb4fc9b6c8664c6bf92dc8f494cfbf2bd8a075ac
1 parent 70f6f31 commit 183cbf7

3 files changed

Lines changed: 305 additions & 23 deletions

File tree

cinderx/Jit/inline_cache.cpp

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,40 @@ TypeWatcher<LoadMethodCache> lm_watcher;
7676
TypeWatcher<LoadTypeMethodCache> ltm_watcher;
7777
constexpr 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;

cinderx/Jit/inline_cache.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,17 @@ class LoadMethodCache {
300300
public:
301301
struct Entry {
302302
BorrowedRef<PyTypeObject> type;
303-
BorrowedRef<> value;
303+
// Borrowed cached attribute, tagged in the low bit. When the low bit is
304+
// clear the value is an untagged PyObject* for a bound method (the common,
305+
// hot case) -- a single bit test selects it and it is bound to the
306+
// receiver. When the low bit is set the value is not a bound method:
307+
// * value == 1 (just the tag bit): the attribute is absent from the type
308+
// and must be resolved via __getattr__ / __getattribute__ dispatch.
309+
// * value > 1: a tagged PyObject* for a staticmethod descriptor or class
310+
// variable; untag it and return it as a plain attribute (no self
311+
// binding).
312+
// Use the tag helpers in inline_cache.cpp to read it.
313+
uintptr_t value{0};
304314
uint32_t keys_version;
305315

306316
// For a NULL sentinel entry (value == nullptr), records whether the type
@@ -311,6 +321,7 @@ class LoadMethodCache {
311321

312322
bool isValidKeysVersion(BorrowedRef<> obj);
313323
};
324+
static_assert(sizeof(Entry) == 24, "Entry must be small");
314325

315326
~LoadMethodCache();
316327

@@ -329,7 +340,8 @@ class LoadMethodCache {
329340
BorrowedRef<PyTypeObject> type,
330341
BorrowedRef<> value,
331342
BorrowedRef<> name,
332-
bool has_getattr_hook);
343+
bool has_getattr_hook,
344+
bool is_bound_method = true);
333345

334346
std::array<Entry, 4> entries_;
335347
std::unique_ptr<CacheStats> cache_stats_;

0 commit comments

Comments
 (0)