Skip to content

Commit ab5a85d

Browse files
Merge branch 'main' of https://github.com/python/cpython into jit-bloom
2 parents 35dfa53 + 83f33dc commit ab5a85d

File tree

14 files changed

+310
-71
lines changed

14 files changed

+310
-71
lines changed

Include/internal/pycore_ceval.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ _PyCallMethodDescriptorFastWithKeywords_StackRef(
461461
int total_args);
462462

463463
PyAPI_FUNC(PyObject *)
464-
_Py_CallBuiltinClass_StackRefSteal(
464+
_Py_CallBuiltinClass_StackRef(
465465
_PyStackRef callable,
466466
_PyStackRef *arguments,
467467
int total_args);

Include/internal/pycore_opcode_metadata.h

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_uop_ids.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_uop_metadata.h

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_capi/test_opt.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,29 @@ class Foo:
15211521
Foo.attr = 0
15221522
self.assertFalse(ex.is_valid())
15231523

1524+
def test_guard_type_version_locked_removed(self):
1525+
"""
1526+
Verify that redundant _GUARD_TYPE_VERSION_LOCKED guards are
1527+
eliminated for sequential STORE_ATTR_INSTANCE_VALUE in __init__.
1528+
"""
1529+
1530+
class Foo:
1531+
def __init__(self):
1532+
self.a = 1
1533+
self.b = 2
1534+
self.c = 3
1535+
1536+
def thing(n):
1537+
for _ in range(n):
1538+
Foo()
1539+
1540+
res, ex = self._run_with_optimizer(thing, TIER2_THRESHOLD)
1541+
self.assertIsNotNone(ex)
1542+
opnames = list(iter_opnames(ex))
1543+
guard_locked_count = opnames.count("_GUARD_TYPE_VERSION_LOCKED")
1544+
# Only the first store needs the guard; the rest should be NOPed.
1545+
self.assertEqual(guard_locked_count, 1)
1546+
15241547
def test_type_version_doesnt_segfault(self):
15251548
"""
15261549
Tests that setting a type version doesn't cause a segfault when later looking at the stack.
@@ -1542,6 +1565,98 @@ def fn(a):
15421565

15431566
fn(A())
15441567

1568+
def test_init_resolves_callable(self):
1569+
"""
1570+
_CHECK_AND_ALLOCATE_OBJECT should resolve __init__ to a constant,
1571+
enabling the optimizer to propagate type information through the frame
1572+
and eliminate redundant function version and arg count checks.
1573+
"""
1574+
class MyPoint:
1575+
def __init__(self, x, y):
1576+
# If __init__ callable is propagated through, then
1577+
# These will get promoted from globals to constants.
1578+
self.x = range(1)
1579+
self.y = range(1)
1580+
1581+
def testfunc(n):
1582+
for _ in range(n):
1583+
p = MyPoint(1.0, 2.0)
1584+
1585+
_, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
1586+
self.assertIsNotNone(ex)
1587+
uops = get_opnames(ex)
1588+
# The __init__ call should be traced through via _PUSH_FRAME
1589+
self.assertIn("_PUSH_FRAME", uops)
1590+
# __init__ resolution allows promotion of range to constant
1591+
self.assertNotIn("_LOAD_GLOBAL_BUILTINS", uops)
1592+
1593+
def test_guard_type_version_locked_propagates(self):
1594+
"""
1595+
_GUARD_TYPE_VERSION_LOCKED should set the type version on the
1596+
symbol so repeated accesses to the same type can benefit.
1597+
"""
1598+
class Item:
1599+
def __init__(self, val):
1600+
self.val = val
1601+
1602+
def get(self):
1603+
return self.val
1604+
1605+
def get2(self):
1606+
return self.val + 1
1607+
1608+
def testfunc(n):
1609+
item = Item(42)
1610+
total = 0
1611+
for _ in range(n):
1612+
# Two method calls on the same object — the second
1613+
# should benefit from type info set by the first.
1614+
total += item.get() + item.get2()
1615+
return total
1616+
1617+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
1618+
self.assertEqual(res, TIER2_THRESHOLD * (42 + 43))
1619+
self.assertIsNotNone(ex)
1620+
uops = get_opnames(ex)
1621+
# Both methods should be traced through
1622+
self.assertEqual(uops.count("_PUSH_FRAME"), 2)
1623+
# Type version propagation: one guard covers both method lookups
1624+
self.assertEqual(uops.count("_GUARD_TYPE_VERSION"), 1)
1625+
# Function checks eliminated (type info resolves the callable)
1626+
self.assertNotIn("_CHECK_FUNCTION_VERSION", uops)
1627+
self.assertNotIn("_CHECK_FUNCTION_EXACT_ARGS", uops)
1628+
1629+
def test_method_chain_guard_elimination(self):
1630+
"""
1631+
Calling two methods on the same object should share the outer
1632+
type guard — only one _GUARD_TYPE_VERSION for the two lookups.
1633+
"""
1634+
class Calc:
1635+
def __init__(self, val):
1636+
self.val = val
1637+
1638+
def add(self, x):
1639+
self.val += x
1640+
return self
1641+
1642+
def testfunc(n):
1643+
c = Calc(0)
1644+
for _ in range(n):
1645+
c.add(1).add(2)
1646+
return c.val
1647+
1648+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
1649+
self.assertEqual(res, TIER2_THRESHOLD * 3)
1650+
self.assertIsNotNone(ex)
1651+
uops = get_opnames(ex)
1652+
# Both add() calls should be inlined
1653+
push_count = uops.count("_PUSH_FRAME")
1654+
self.assertEqual(push_count, 2)
1655+
# Only one outer type version guard for the two method lookups
1656+
# on the same object c (the second lookup reuses type info)
1657+
guard_version_count = uops.count("_GUARD_TYPE_VERSION")
1658+
self.assertEqual(guard_version_count, 1)
1659+
15451660
def test_func_guards_removed_or_reduced(self):
15461661
def testfunc(n):
15471662
for i in range(n):
@@ -4547,6 +4662,24 @@ def return_true():
45474662
# v + 1 should be constant folded
45484663
self.assertNotIn("_BINARY_OP", uops)
45494664

4665+
def test_is_none_narrows_to_constant(self):
4666+
def testfunc(n):
4667+
value = None
4668+
hits = 0
4669+
for _ in range(n):
4670+
if value is None:
4671+
hits += 1
4672+
return hits
4673+
4674+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
4675+
self.assertEqual(res, TIER2_THRESHOLD)
4676+
self.assertIsNotNone(ex)
4677+
uops = get_opnames(ex)
4678+
4679+
self.assertNotIn("_IS_NONE", uops)
4680+
self.assertIn("_GUARD_IS_NONE_POP", uops)
4681+
self.assertIn("_POP_TOP_NOP", uops)
4682+
45504683
def test_is_false_narrows_to_constant(self):
45514684
def f(n):
45524685
def return_false():

Modules/_testinternalcapi/test_cases.c.h

Lines changed: 23 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/bytecodes.c

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3011,6 +3011,7 @@ dummy_func(
30113011

30123012
macro(STORE_ATTR_INSTANCE_VALUE) =
30133013
unused/1 +
3014+
_RECORD_TOS_TYPE +
30143015
_LOCK_OBJECT +
30153016
_GUARD_TYPE_VERSION_LOCKED +
30163017
_GUARD_DORV_NO_DICT +
@@ -4572,23 +4573,24 @@ dummy_func(
45724573
EXIT_IF(tp->tp_vectorcall == NULL);
45734574
}
45744575

4575-
op(_CALL_BUILTIN_CLASS, (callable, self_or_null, args[oparg] -- res)) {
4576+
op(_CALL_BUILTIN_CLASS, (callable, self_or_null, args[oparg] -- callable, self_or_null, args[oparg])) {
45764577
int total_args = oparg;
45774578
_PyStackRef *arguments = args;
45784579
if (!PyStackRef_IsNull(self_or_null)) {
45794580
arguments--;
45804581
total_args++;
45814582
}
45824583
STAT_INC(CALL, hit);
4583-
PyObject *res_o = _Py_CallBuiltinClass_StackRefSteal(
4584+
PyObject *res_o = _Py_CallBuiltinClass_StackRef(
45844585
callable,
45854586
arguments,
45864587
total_args);
4587-
DEAD(args);
4588-
DEAD(self_or_null);
4589-
DEAD(callable);
4590-
ERROR_IF(res_o == NULL);
4591-
res = PyStackRef_FromPyObjectSteal(res_o);
4588+
if (res_o == NULL) {
4589+
ERROR_NO_POP();
4590+
}
4591+
_PyStackRef temp = callable;
4592+
callable = PyStackRef_FromPyObjectSteal(res_o);
4593+
PyStackRef_CLOSE(temp);
45924594
}
45934595

45944596
macro(CALL_BUILTIN_CLASS) =
@@ -4597,6 +4599,8 @@ dummy_func(
45974599
unused/2 +
45984600
_GUARD_CALLABLE_BUILTIN_CLASS +
45994601
_CALL_BUILTIN_CLASS +
4602+
_POP_TOP_OPARG +
4603+
POP_TOP +
46004604
_CHECK_PERIODIC_AT_END;
46014605

46024606
op(_GUARD_CALLABLE_BUILTIN_O, (callable, self_or_null, args[oparg] -- callable, self_or_null, args[oparg])) {

0 commit comments

Comments
 (0)