Skip to content

Commit d7c4538

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Don't reset all inline cache types when one changes
Summary: Clearing the whole cache seems counter productive... we seem to be doing this because we also watch the descriptor type when it's a data descriptor. Instead let's check to see if we have a data descriptor and invalidate that type when the descriptor changes. This is a little more complicated than it seems because of finalization at shutdown. The descriptor could be freed as part of cyclic trash. Worse still the descriptor could live on a base type of the type that we're freeing. And worse still is that the weak references tracked in `tp_subclasses` could be cleared on the base type before the base type is cleared causing us not to receive the type modification event for the sub type. For this reason we need to track the descriptors type and not rely on doing `Py_TYPE(descr)` when we're finalizing our caches. Reviewed By: alexmalyshev Differential Revision: D104697182 fbshipit-source-id: 25d421b436bc91538df54d245edf50b71a27b648
1 parent a2d82dc commit d7c4538

3 files changed

Lines changed: 137 additions & 7 deletions

File tree

cinderx/Jit/inline_cache.cpp

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ struct TypeWatcher {
2727
jit::UnorderedMap<BorrowedRef<PyTypeObject>, jit::UnorderedSet<T*>> caches;
2828

2929
void watch(BorrowedRef<PyTypeObject> type, T* cache) {
30+
if (PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE)) {
31+
// We don't watch immutable types - they can't be modified.
32+
return;
33+
}
3034
JIT_CHECK(
3135
cinderx::getModuleState()->watcher_state.watchType(type) == 0,
3236
"Failed to watch type {} for attribute cache",
@@ -43,20 +47,30 @@ struct TypeWatcher {
4347
// don't unwatch type; other watchers may still be watching it
4448
}
4549

46-
void typeChanged(BorrowedRef<PyTypeObject> type) {
50+
// Notify caches watching a type. Calls Callback(cache, type) for each
51+
// registered cache. The default callback calls cache->typeChanged(type).
52+
template <typename Callback>
53+
void typeChanged(BorrowedRef<PyTypeObject> type, Callback cb) {
4754
auto it = caches.find(type);
4855
if (it == caches.end()) {
4956
return;
5057
}
5158
jit::UnorderedSet<T*> to_notify = std::move(it->second);
5259
caches.erase(it);
5360
for (T* cache : to_notify) {
54-
cache->typeChanged(type);
61+
cb(cache, type);
5562
}
5663
}
64+
65+
void typeChanged(BorrowedRef<PyTypeObject> type) {
66+
typeChanged(type, [](T* cache, BorrowedRef<PyTypeObject> tp) {
67+
cache->typeChanged(tp);
68+
});
69+
}
5770
};
5871

5972
TypeWatcher<AttributeCache> ac_watcher;
73+
TypeWatcher<AttributeCache> ac_descr_watcher;
6074
TypeWatcher<LoadTypeAttrCache> ltac_watcher;
6175
TypeWatcher<LoadMethodCache> lm_watcher;
6276
TypeWatcher<LoadTypeMethodCache> ltm_watcher;
@@ -563,6 +577,7 @@ void AttributeMutator::set_combined(PyTypeObject* type) {
563577
void AttributeMutator::set_data_descr(PyTypeObject* type, PyObject* descr) {
564578
set_type(type, Kind::kDataDescr);
565579
data_descr_.descr = descr;
580+
data_descr_.descr_type = Py_TYPE(descr);
566581
}
567582

568583
void AttributeMutator::set_member_descr(PyTypeObject* type, PyObject* descr) {
@@ -589,6 +604,13 @@ void AttributeMutator::set_split(
589604
split_.keys = keys;
590605
}
591606

607+
BorrowedRef<PyTypeObject> AttributeMutator::watchedDescrType() const {
608+
if (get_kind() == Kind::kDataDescr) {
609+
return data_descr_.descr_type;
610+
}
611+
return nullptr;
612+
}
613+
592614
inline int
593615
AttributeMutator::setAttr(PyObject* obj, PyObject* name, PyObject* value) {
594616
JIT_CHECK(
@@ -675,14 +697,61 @@ AttributeCache::~AttributeCache() {
675697
for (auto& entry : entries()) {
676698
if (entry.type() != nullptr) {
677699
ac_watcher.unwatch(entry.type(), this);
700+
BorrowedRef<PyTypeObject> descr_tp = entry.watchedDescrType();
701+
if (descr_tp != nullptr) {
702+
ac_descr_watcher.unwatch(descr_tp, this);
703+
}
678704
entry.reset();
679705
}
680706
}
681707
}
682708

683-
void AttributeCache::typeChanged(PyTypeObject*) {
709+
void AttributeCache::typeChanged(PyTypeObject* tp) {
684710
for (auto& entry : entries()) {
685-
entry.reset();
711+
if (entry.type() == tp) {
712+
BorrowedRef<PyTypeObject> descr_tp = entry.watchedDescrType();
713+
// Reset the entry, this also resets the kind so after the call
714+
// watchedDescrType returns nullptr.
715+
entry.reset();
716+
if (descr_tp != nullptr) {
717+
// For entry.type() the cache is 1 to 1 between types and the cache.
718+
// For descriptors the same descriptor can be shared multiple times
719+
// per a type. So check if we have any descriptors remaining and only
720+
// unwatch when they're all clear.
721+
bool found = false;
722+
for (auto& other : entries()) {
723+
if (other.watchedDescrType() == descr_tp) {
724+
found = true;
725+
break;
726+
}
727+
}
728+
if (!found) {
729+
// there are no other entries watching this descriptor.
730+
ac_descr_watcher.unwatch(descr_tp, this);
731+
}
732+
}
733+
}
734+
}
735+
}
736+
737+
void AttributeCache::descrTypeChanged(PyTypeObject* tp) {
738+
// We only need to unwatch this AttributeCache in the ac_descr_watcher
739+
// once as it applies to all entries so we use a flag to track if we've
740+
// already unwatched it for multiple entries (it wouldn't hurt to try
741+
// and unwatch multiple times but it's a couple of hash lookups)
742+
bool found = false;
743+
for (auto& entry : entries()) {
744+
if (entry.watchedDescrType() == tp) {
745+
// If we were watching the descriptor the whole cache entry is
746+
// invalidated and we no longer need to watch the type.
747+
ac_watcher.unwatch(entry.type(), this);
748+
entry.reset();
749+
if (!found) {
750+
// unwatch the descriptor watcher once for the whole cache
751+
ac_descr_watcher.unwatch(tp, this);
752+
found = true;
753+
}
754+
}
686755
}
687756
}
688757

@@ -766,9 +835,10 @@ void AttributeCache::fill(
766835
if (descr_type == &PyMemberDescr_Type) {
767836
mut->set_member_descr(type, descr);
768837
} else {
769-
// If someone deletes descr_types's __set__ method, it will no longer
770-
// be a data descriptor, and the cache kind has to change.
771-
ac_watcher.watch(descr_type, this);
838+
// If someone modifies descr_type (e.g., deletes __set__), it may no
839+
// longer be a data descriptor. Watch it via the descriptor watcher
840+
// so the cache is invalidated.
841+
ac_descr_watcher.watch(descr_type, this);
772842
mut->set_data_descr(type, descr);
773843
}
774844
} else {
@@ -1602,6 +1672,10 @@ LoadModuleMethodCache::lookupSlowPath(BorrowedRef<> obj, BorrowedRef<> name) {
16021672

16031673
void notifyICsTypeChanged(BorrowedRef<PyTypeObject> type) {
16041674
ac_watcher.typeChanged(type);
1675+
ac_descr_watcher.typeChanged(
1676+
type, [](AttributeCache* cache, BorrowedRef<PyTypeObject> tp) {
1677+
cache->descrTypeChanged(tp);
1678+
});
16051679
ltac_watcher.typeChanged(type);
16061680
lm_watcher.typeChanged(type);
16071681
ltm_watcher.typeChanged(type);

cinderx/Jit/inline_cache.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ struct DataDescrMutator {
5555
int setAttr(PyObject* obj, PyObject* value);
5656

5757
BorrowedRef<> descr;
58+
BorrowedRef<PyTypeObject> descr_type;
5859
};
5960

6061
// Mutator for a member descriptor
@@ -111,6 +112,7 @@ class AttributeMutator {
111112
Py_ssize_t val_offset,
112113
PyDictKeysObject* keys,
113114
bool values_inline);
115+
BorrowedRef<PyTypeObject> watchedDescrType() const;
114116

115117
PyObject* getAttr(PyObject* obj, PyObject* name);
116118
int setAttr(PyObject* obj, PyObject* name, PyObject* value);
@@ -139,6 +141,7 @@ class AttributeCache {
139141
~AttributeCache();
140142

141143
void typeChanged(PyTypeObject* type);
144+
void descrTypeChanged(PyTypeObject* type);
142145

143146
protected:
144147
std::span<AttributeMutator> entries();

cinderx/PythonLib/test_cinderx/test_jit_attr_cache.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,59 @@ def get_attr(o):
430430
C.__dict__["foo"].__class__ = Descr
431431
self.assertEqual(get_attr(c), "get")
432432

433+
def test_shared_descr_type_across_cache_entries(self):
434+
"""Two types share the same descriptor type in the same cache.
435+
436+
When one type changes and its cache entry is reset, the descriptor
437+
type watch must survive for the remaining entry. Otherwise mutating
438+
the descriptor type (e.g. removing __set__) won't invalidate the
439+
surviving entry.
440+
"""
441+
442+
class Descr:
443+
def __get__(self, obj, ty):
444+
return "descr"
445+
446+
def __set__(self, obj, val):
447+
raise RuntimeError("unimplemented")
448+
449+
class T1:
450+
foo = Descr()
451+
452+
class T2:
453+
foo = Descr()
454+
455+
@cinder_support.failUnlessJITCompiled
456+
def get_attr(o):
457+
return o.foo
458+
459+
t1 = T1()
460+
t2 = T2()
461+
462+
# Prime and cache both entries (same descriptor type Descr).
463+
self.assertEqual(get_attr(t1), "descr")
464+
self.assertEqual(get_attr(t1), "descr")
465+
self.assertEqual(get_attr(t2), "descr")
466+
self.assertEqual(get_attr(t2), "descr")
467+
468+
# Both t1 and t2 have instance dict entries shadowed by
469+
# the data descriptor.
470+
t1.__dict__["foo"] = "t1 attr"
471+
t2.__dict__["foo"] = "t2 attr"
472+
self.assertEqual(get_attr(t1), "descr")
473+
self.assertEqual(get_attr(t2), "descr")
474+
475+
# Invalidate T1's cache entry by mutating T1. This must NOT
476+
# unwatch Descr from the descriptor watcher since T2's entry
477+
# still depends on it.
478+
T1.bar = 1
479+
480+
# Now mutate the descriptor type: removing __set__ makes Descr
481+
# no longer a data descriptor. T2's entry must be invalidated
482+
# so the instance attribute is returned instead.
483+
del Descr.__set__
484+
self.assertEqual(get_attr(t2), "t2 attr")
485+
433486
@passIf(
434487
cinderx.jit.is_enabled(),
435488
"T214641462: Not clear why this is failing, but it is",

0 commit comments

Comments
 (0)