Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Bring back tests
  • Loading branch information
DinoV committed Jun 5, 2026
commit fabf182924a8a9b8204abcb049d2d74fe50aa0b1
6 changes: 3 additions & 3 deletions Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,15 @@ struct _dictkeysobject {
see the DK_ENTRIES() / DK_UNICODE_ENTRIES() functions below */
};

struct _dictsharedkeysobject {
struct _instancekeysobject {
PyTypeObject* dsk_owning_type;
struct _dictkeysobject dsk_keys;
};

static inline struct _dictsharedkeysobject *_PyDictKeys_AsSharedKeys(struct _dictkeysobject *keys)
static inline struct _instancekeysobject *_PyDictKeys_AsSharedKeys(struct _dictkeysobject *keys)
{
assert(keys->dk_kind == DICT_KEYS_SPLIT);
return _Py_CONTAINER_OF(keys, struct _dictsharedkeysobject, dsk_keys);
return _Py_CONTAINER_OF(keys, struct _instancekeysobject, dsk_keys);
}

/* This must be no more than 250, for the prefix size to fit in one byte. */
Expand Down
153 changes: 153 additions & 0 deletions Lib/test/test_capi/test_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,125 @@ def test_modified_local_is_seen_by_optimized_code(self):
self.assertIs(type(s), float)
self.assertEqual(s, 1024.0)

def test_guard_type_version_removed(self):
def thing(a):
x = 0
for _ in range(TIER2_THRESHOLD):
x += a.attr
x += a.attr
return x

class Foo:
attr = 1

res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))
self.assertIsNotNone(ex)
self.assertEqual(res, TIER2_THRESHOLD * 2)
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
self.assertEqual(guard_type_version_count, 1)

def test_guard_type_version_removed_inlined(self):
"""
Verify that the guard type version if we have an inlined function
"""

def fn():
pass

def thing(a):
x = 0
for _ in range(TIER2_THRESHOLD):
x += a.attr
fn()
x += a.attr
return x

class Foo:
attr = 1

res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))
self.assertIsNotNone(ex)
self.assertEqual(res, TIER2_THRESHOLD * 2)
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
self.assertEqual(guard_type_version_count, 1)

def test_guard_type_version_removed_invalidation(self):

def thing(a):
x = 0
for i in range(TIER2_THRESHOLD + 1):
x += a.attr
# The first TIER2_THRESHOLD iterations we set the attribute on
# this dummy class, which shouldn't trigger the type watcher.
# Note that the code needs to be in this weird form so it's
# optimized inline without any control flow:
setattr((Bar, Foo)[i == TIER2_THRESHOLD + 1], "attr", 2)
x += a.attr
return x

class Foo:
attr = 1

class Bar:
pass

res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))
self.assertEqual(res, TIER2_THRESHOLD * 2 + 2)
call = opnames.index("_CALL_BUILTIN_FAST")
load_attr_top = opnames.index("_LOAD_CONST_INLINE_BORROW", 0, call)
load_attr_bottom = opnames.index("_LOAD_CONST_INLINE_BORROW", call)
self.assertEqual(opnames[:load_attr_top].count("_GUARD_TYPE_VERSION"), 1)
self.assertEqual(opnames[call:load_attr_bottom].count("_CHECK_VALIDITY"), 2)

def test_guard_type_version_removed_escaping(self):

def thing(a):
x = 0
for i in range(TIER2_THRESHOLD):
x += a.attr
# eval should be escaping
eval("None")
x += a.attr
return x

class Foo:
attr = 1
res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))
self.assertIsNotNone(ex)
self.assertEqual(res, TIER2_THRESHOLD * 2)
call = opnames.index("_CALL_BUILTIN_FAST_WITH_KEYWORDS")
load_attr_top = opnames.index("_LOAD_CONST_INLINE_BORROW", 0, call)
load_attr_bottom = opnames.index("_LOAD_CONST_INLINE_BORROW", call)
self.assertEqual(opnames[:load_attr_top].count("_GUARD_TYPE_VERSION"), 1)
self.assertEqual(opnames[call:load_attr_bottom].count("_CHECK_VALIDITY"), 2)

def test_guard_type_version_executor_invalidated(self):
"""
Verify that the executor is invalided on a type change.
"""

def thing(a):
x = 0
for i in range(TIER2_THRESHOLD):
x += a.attr
x += a.attr
return x

class Foo:
attr = 1

res, ex = self._run_with_optimizer(thing, Foo())
self.assertEqual(res, TIER2_THRESHOLD * 2)
self.assertIsNotNone(ex)
self.assertEqual(list(iter_opnames(ex)).count("_GUARD_TYPE_VERSION"), 1)
self.assertTrue(ex.is_valid())
Foo.attr = 0
self.assertFalse(ex.is_valid())

def test_guard_type_version_locked_removed(self):
"""
Verify that redundant _GUARD_TYPE_VERSION_LOCKED guards are
Expand Down Expand Up @@ -1736,9 +1855,42 @@ def testfunc(n):
uops = get_opnames(ex)
# Both methods should be traced through
self.assertEqual(uops.count("_PUSH_FRAME"), 2)
# Type version propagation: one guard covers both method lookups
self.assertEqual(uops.count("_GUARD_TYPE_VERSION"), 1)
# Function checks cannot be eliminated for safety reasons.
self.assertIn("_CHECK_FUNCTION_VERSION", uops)

def test_method_chain_guard_elimination(self):
"""
Calling two methods on the same object should share the outer
type guard — only one _GUARD_TYPE_VERSION for the two lookups.
"""
class Calc:
def __init__(self, val):
self.val = val

def add(self, x):
self.val += x
return self

def testfunc(n):
c = Calc(0)
for _ in range(n):
c.add(1).add(2)
return c.val

res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertEqual(res, TIER2_THRESHOLD * 3)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
# Both add() calls should be inlined
push_count = uops.count("_PUSH_FRAME")
self.assertEqual(push_count, 2)
# Only one outer type version guard for the two method lookups
# on the same object c (the second lookup reuses type info)
guard_version_count = uops.count("_GUARD_TYPE_VERSION")
self.assertEqual(guard_version_count, 1)

def test_func_guards_removed_or_reduced(self):
def testfunc(n):
for i in range(n):
Expand Down Expand Up @@ -3485,6 +3637,7 @@ def testfunc(n):
self.assertEqual(res, 2 * TIER2_THRESHOLD)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertIn("_GUARD_TYPE_VERSION", uops)
self.assertNotIn("_CHECK_ATTR_CLASS", uops)

def test_load_common_constant(self):
Expand Down
10 changes: 5 additions & 5 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ free_keys_object(PyDictKeysObject *keys, bool use_qsbr)
if (keys->dk_kind == DICT_KEYS_SPLIT) {
ptr = _PyDictKeys_AsSharedKeys(keys);
#ifdef Py_GIL_DISABLED
size += offsetof(struct _dictsharedkeysobject, dsk_keys);
size += offsetof(struct _instancekeysobject, dsk_keys);
#endif
}
#ifdef Py_GIL_DISABLED
Expand Down Expand Up @@ -7129,8 +7129,8 @@ _PyDict_NewKeysForClass(PyHeapTypeObject *cls)
int log2_bytes = get_log2_bytes(NEXT_LOG2_SHARED_KEYS_MAX_SIZE);
Py_ssize_t usable = USABLE_FRACTION((size_t)1<<NEXT_LOG2_SHARED_KEYS_MAX_SIZE);

struct _dictsharedkeysobject *shared_keys =
PyMem_Malloc(sizeof(struct _dictsharedkeysobject)
struct _instancekeysobject *shared_keys =
PyMem_Malloc(sizeof(struct _instancekeysobject)
+ ((size_t)1 << log2_bytes)
+ sizeof(PyDictUnicodeEntry) * usable);
if (shared_keys == NULL) {
Expand Down Expand Up @@ -7164,7 +7164,7 @@ _PyDict_NewKeysForClass(PyHeapTypeObject *cls)
void
_PyDict_RemoveKeysForClass(PyHeapTypeObject *cls)
{
struct _dictsharedkeysobject *shared_keys = _PyDictKeys_AsSharedKeys(cls->ht_cached_keys);
struct _instancekeysobject *shared_keys = _PyDictKeys_AsSharedKeys(cls->ht_cached_keys);
FT_ATOMIC_STORE_PTR_RELEASE(shared_keys->dsk_owning_type, NULL);

_PyDictKeys_DecRef(cls->ht_cached_keys);
Expand All @@ -7173,7 +7173,7 @@ _PyDict_RemoveKeysForClass(PyHeapTypeObject *cls)
void
_PyDict_SplitKeysInvalidated(PyDictKeysObject* keys)
{
struct _dictsharedkeysobject *shared_keys = _PyDictKeys_AsSharedKeys(keys);
struct _instancekeysobject *shared_keys = _PyDictKeys_AsSharedKeys(keys);
PyTypeObject *type = FT_ATOMIC_LOAD_PTR_ACQUIRE(shared_keys->dsk_owning_type);
if (type) {
PyType_Modified(type);
Expand Down
Loading