Skip to content

Commit fabf182

Browse files
committed
Bring back tests
1 parent 2d9799f commit fabf182

3 files changed

Lines changed: 161 additions & 8 deletions

File tree

Include/internal/pycore_dict.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,15 +241,15 @@ struct _dictkeysobject {
241241
see the DK_ENTRIES() / DK_UNICODE_ENTRIES() functions below */
242242
};
243243

244-
struct _dictsharedkeysobject {
244+
struct _instancekeysobject {
245245
PyTypeObject* dsk_owning_type;
246246
struct _dictkeysobject dsk_keys;
247247
};
248248

249-
static inline struct _dictsharedkeysobject *_PyDictKeys_AsSharedKeys(struct _dictkeysobject *keys)
249+
static inline struct _instancekeysobject *_PyDictKeys_AsSharedKeys(struct _dictkeysobject *keys)
250250
{
251251
assert(keys->dk_kind == DICT_KEYS_SPLIT);
252-
return _Py_CONTAINER_OF(keys, struct _dictsharedkeysobject, dsk_keys);
252+
return _Py_CONTAINER_OF(keys, struct _instancekeysobject, dsk_keys);
253253
}
254254

255255
/* This must be no more than 250, for the prefix size to fit in one byte. */

Lib/test/test_capi/test_opt.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,125 @@ def test_modified_local_is_seen_by_optimized_code(self):
16011601
self.assertIs(type(s), float)
16021602
self.assertEqual(s, 1024.0)
16031603

1604+
def test_guard_type_version_removed(self):
1605+
def thing(a):
1606+
x = 0
1607+
for _ in range(TIER2_THRESHOLD):
1608+
x += a.attr
1609+
x += a.attr
1610+
return x
1611+
1612+
class Foo:
1613+
attr = 1
1614+
1615+
res, ex = self._run_with_optimizer(thing, Foo())
1616+
opnames = list(iter_opnames(ex))
1617+
self.assertIsNotNone(ex)
1618+
self.assertEqual(res, TIER2_THRESHOLD * 2)
1619+
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
1620+
self.assertEqual(guard_type_version_count, 1)
1621+
1622+
def test_guard_type_version_removed_inlined(self):
1623+
"""
1624+
Verify that the guard type version if we have an inlined function
1625+
"""
1626+
1627+
def fn():
1628+
pass
1629+
1630+
def thing(a):
1631+
x = 0
1632+
for _ in range(TIER2_THRESHOLD):
1633+
x += a.attr
1634+
fn()
1635+
x += a.attr
1636+
return x
1637+
1638+
class Foo:
1639+
attr = 1
1640+
1641+
res, ex = self._run_with_optimizer(thing, Foo())
1642+
opnames = list(iter_opnames(ex))
1643+
self.assertIsNotNone(ex)
1644+
self.assertEqual(res, TIER2_THRESHOLD * 2)
1645+
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
1646+
self.assertEqual(guard_type_version_count, 1)
1647+
1648+
def test_guard_type_version_removed_invalidation(self):
1649+
1650+
def thing(a):
1651+
x = 0
1652+
for i in range(TIER2_THRESHOLD + 1):
1653+
x += a.attr
1654+
# The first TIER2_THRESHOLD iterations we set the attribute on
1655+
# this dummy class, which shouldn't trigger the type watcher.
1656+
# Note that the code needs to be in this weird form so it's
1657+
# optimized inline without any control flow:
1658+
setattr((Bar, Foo)[i == TIER2_THRESHOLD + 1], "attr", 2)
1659+
x += a.attr
1660+
return x
1661+
1662+
class Foo:
1663+
attr = 1
1664+
1665+
class Bar:
1666+
pass
1667+
1668+
res, ex = self._run_with_optimizer(thing, Foo())
1669+
opnames = list(iter_opnames(ex))
1670+
self.assertEqual(res, TIER2_THRESHOLD * 2 + 2)
1671+
call = opnames.index("_CALL_BUILTIN_FAST")
1672+
load_attr_top = opnames.index("_LOAD_CONST_INLINE_BORROW", 0, call)
1673+
load_attr_bottom = opnames.index("_LOAD_CONST_INLINE_BORROW", call)
1674+
self.assertEqual(opnames[:load_attr_top].count("_GUARD_TYPE_VERSION"), 1)
1675+
self.assertEqual(opnames[call:load_attr_bottom].count("_CHECK_VALIDITY"), 2)
1676+
1677+
def test_guard_type_version_removed_escaping(self):
1678+
1679+
def thing(a):
1680+
x = 0
1681+
for i in range(TIER2_THRESHOLD):
1682+
x += a.attr
1683+
# eval should be escaping
1684+
eval("None")
1685+
x += a.attr
1686+
return x
1687+
1688+
class Foo:
1689+
attr = 1
1690+
res, ex = self._run_with_optimizer(thing, Foo())
1691+
opnames = list(iter_opnames(ex))
1692+
self.assertIsNotNone(ex)
1693+
self.assertEqual(res, TIER2_THRESHOLD * 2)
1694+
call = opnames.index("_CALL_BUILTIN_FAST_WITH_KEYWORDS")
1695+
load_attr_top = opnames.index("_LOAD_CONST_INLINE_BORROW", 0, call)
1696+
load_attr_bottom = opnames.index("_LOAD_CONST_INLINE_BORROW", call)
1697+
self.assertEqual(opnames[:load_attr_top].count("_GUARD_TYPE_VERSION"), 1)
1698+
self.assertEqual(opnames[call:load_attr_bottom].count("_CHECK_VALIDITY"), 2)
1699+
1700+
def test_guard_type_version_executor_invalidated(self):
1701+
"""
1702+
Verify that the executor is invalided on a type change.
1703+
"""
1704+
1705+
def thing(a):
1706+
x = 0
1707+
for i in range(TIER2_THRESHOLD):
1708+
x += a.attr
1709+
x += a.attr
1710+
return x
1711+
1712+
class Foo:
1713+
attr = 1
1714+
1715+
res, ex = self._run_with_optimizer(thing, Foo())
1716+
self.assertEqual(res, TIER2_THRESHOLD * 2)
1717+
self.assertIsNotNone(ex)
1718+
self.assertEqual(list(iter_opnames(ex)).count("_GUARD_TYPE_VERSION"), 1)
1719+
self.assertTrue(ex.is_valid())
1720+
Foo.attr = 0
1721+
self.assertFalse(ex.is_valid())
1722+
16041723
def test_guard_type_version_locked_removed(self):
16051724
"""
16061725
Verify that redundant _GUARD_TYPE_VERSION_LOCKED guards are
@@ -1736,9 +1855,42 @@ def testfunc(n):
17361855
uops = get_opnames(ex)
17371856
# Both methods should be traced through
17381857
self.assertEqual(uops.count("_PUSH_FRAME"), 2)
1858+
# Type version propagation: one guard covers both method lookups
1859+
self.assertEqual(uops.count("_GUARD_TYPE_VERSION"), 1)
17391860
# Function checks cannot be eliminated for safety reasons.
17401861
self.assertIn("_CHECK_FUNCTION_VERSION", uops)
17411862

1863+
def test_method_chain_guard_elimination(self):
1864+
"""
1865+
Calling two methods on the same object should share the outer
1866+
type guard — only one _GUARD_TYPE_VERSION for the two lookups.
1867+
"""
1868+
class Calc:
1869+
def __init__(self, val):
1870+
self.val = val
1871+
1872+
def add(self, x):
1873+
self.val += x
1874+
return self
1875+
1876+
def testfunc(n):
1877+
c = Calc(0)
1878+
for _ in range(n):
1879+
c.add(1).add(2)
1880+
return c.val
1881+
1882+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
1883+
self.assertEqual(res, TIER2_THRESHOLD * 3)
1884+
self.assertIsNotNone(ex)
1885+
uops = get_opnames(ex)
1886+
# Both add() calls should be inlined
1887+
push_count = uops.count("_PUSH_FRAME")
1888+
self.assertEqual(push_count, 2)
1889+
# Only one outer type version guard for the two method lookups
1890+
# on the same object c (the second lookup reuses type info)
1891+
guard_version_count = uops.count("_GUARD_TYPE_VERSION")
1892+
self.assertEqual(guard_version_count, 1)
1893+
17421894
def test_func_guards_removed_or_reduced(self):
17431895
def testfunc(n):
17441896
for i in range(n):
@@ -3485,6 +3637,7 @@ def testfunc(n):
34853637
self.assertEqual(res, 2 * TIER2_THRESHOLD)
34863638
self.assertIsNotNone(ex)
34873639
uops = get_opnames(ex)
3640+
self.assertIn("_GUARD_TYPE_VERSION", uops)
34883641
self.assertNotIn("_CHECK_ATTR_CLASS", uops)
34893642

34903643
def test_load_common_constant(self):

Objects/dictobject.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ free_keys_object(PyDictKeysObject *keys, bool use_qsbr)
866866
if (keys->dk_kind == DICT_KEYS_SPLIT) {
867867
ptr = _PyDictKeys_AsSharedKeys(keys);
868868
#ifdef Py_GIL_DISABLED
869-
size += offsetof(struct _dictsharedkeysobject, dsk_keys);
869+
size += offsetof(struct _instancekeysobject, dsk_keys);
870870
#endif
871871
}
872872
#ifdef Py_GIL_DISABLED
@@ -7129,8 +7129,8 @@ _PyDict_NewKeysForClass(PyHeapTypeObject *cls)
71297129
int log2_bytes = get_log2_bytes(NEXT_LOG2_SHARED_KEYS_MAX_SIZE);
71307130
Py_ssize_t usable = USABLE_FRACTION((size_t)1<<NEXT_LOG2_SHARED_KEYS_MAX_SIZE);
71317131

7132-
struct _dictsharedkeysobject *shared_keys =
7133-
PyMem_Malloc(sizeof(struct _dictsharedkeysobject)
7132+
struct _instancekeysobject *shared_keys =
7133+
PyMem_Malloc(sizeof(struct _instancekeysobject)
71347134
+ ((size_t)1 << log2_bytes)
71357135
+ sizeof(PyDictUnicodeEntry) * usable);
71367136
if (shared_keys == NULL) {
@@ -7164,7 +7164,7 @@ _PyDict_NewKeysForClass(PyHeapTypeObject *cls)
71647164
void
71657165
_PyDict_RemoveKeysForClass(PyHeapTypeObject *cls)
71667166
{
7167-
struct _dictsharedkeysobject *shared_keys = _PyDictKeys_AsSharedKeys(cls->ht_cached_keys);
7167+
struct _instancekeysobject *shared_keys = _PyDictKeys_AsSharedKeys(cls->ht_cached_keys);
71687168
FT_ATOMIC_STORE_PTR_RELEASE(shared_keys->dsk_owning_type, NULL);
71697169

71707170
_PyDictKeys_DecRef(cls->ht_cached_keys);
@@ -7173,7 +7173,7 @@ _PyDict_RemoveKeysForClass(PyHeapTypeObject *cls)
71737173
void
71747174
_PyDict_SplitKeysInvalidated(PyDictKeysObject* keys)
71757175
{
7176-
struct _dictsharedkeysobject *shared_keys = _PyDictKeys_AsSharedKeys(keys);
7176+
struct _instancekeysobject *shared_keys = _PyDictKeys_AsSharedKeys(keys);
71777177
PyTypeObject *type = FT_ATOMIC_LOAD_PTR_ACQUIRE(shared_keys->dsk_owning_type);
71787178
if (type) {
71797179
PyType_Modified(type);

0 commit comments

Comments
 (0)