Skip to content

Commit b71cc87

Browse files
Eclips4iritkatriel
authored andcommitted
gh-148817: Fold long lists/sets of constant elements into constant tuples/frozensets (GH-149016)
Fold long lists/sets of constant elements into constant tuples/frozensets. (cherry picked from commit 084230e) Co-authored-by: Kirill Podoprigora <kirill.bast9@mail.ru> Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
1 parent 39a0217 commit b71cc87

4 files changed

Lines changed: 218 additions & 22 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ Tools/cases_generator/ @markshannon
227227
Python/assemble.c @markshannon @iritkatriel
228228
Python/codegen.c @markshannon @iritkatriel
229229
Python/compile.c @markshannon @iritkatriel
230-
Python/flowgraph.c @markshannon @iritkatriel
230+
Python/flowgraph.c @markshannon @iritkatriel @eclips4
231231
Python/instruction_sequence.c @iritkatriel
232232
Python/symtable.c @JelleZijlstra @carljm
233233

Lib/test/test_peepholer.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,6 +2470,168 @@ def test_list_to_tuple_get_iter_is_safe(self):
24702470
self.assertEqual(b, [3, 2, 1, 0])
24712471
self.assertEqual(items, [])
24722472

2473+
def test_fold_constant_big_list_for_iter(self):
2474+
# for x in [c1, c2, ..., cN] (N > 30) should fold to LOAD_CONST tuple
2475+
consts = 35
2476+
before = (
2477+
[("BUILD_LIST", 0, 1)] +
2478+
[("LOAD_CONST", 0, 2), ("LIST_APPEND", 1, 3)] * consts +
2479+
[("GET_ITER", 0, 4),
2480+
top := self.Label(),
2481+
("FOR_ITER", end := self.Label(), 5),
2482+
("STORE_FAST", 0, 6),
2483+
("JUMP", top, 7),
2484+
end,
2485+
("END_FOR", None, 8),
2486+
("POP_ITER", None, 9),
2487+
("LOAD_CONST", 0, 10),
2488+
("RETURN_VALUE", None, 11)]
2489+
)
2490+
after = [
2491+
("LOAD_CONST", 1, 3),
2492+
("GET_ITER", 0, 4),
2493+
top := self.Label(),
2494+
("FOR_ITER", end := self.Label(), 5),
2495+
("STORE_FAST", 0, 6),
2496+
("JUMP", top, 7),
2497+
end,
2498+
("END_FOR", None, 8),
2499+
("POP_ITER", None, 9),
2500+
("LOAD_CONST", 0, 10),
2501+
("RETURN_VALUE", None, 11),
2502+
]
2503+
result_const = tuple(["test"] * consts)
2504+
self.cfg_optimization_test(before, after, consts=["test"],
2505+
expected_consts=["test", result_const])
2506+
2507+
def test_fold_constant_big_set_for_iter(self):
2508+
# for x in {c1, c2, ..., cN} (N > 30) should fold to LOAD_CONST frozenset
2509+
before = [
2510+
("BUILD_SET", 0, 1),
2511+
("LOAD_SMALL_INT", 1, 2), ("SET_ADD", 1, 3),
2512+
("LOAD_SMALL_INT", 2, 4), ("SET_ADD", 1, 5),
2513+
("LOAD_SMALL_INT", 3, 6), ("SET_ADD", 1, 7),
2514+
("GET_ITER", 0, 8),
2515+
top := self.Label(),
2516+
("FOR_ITER", end := self.Label(), 9),
2517+
("STORE_FAST", 0, 10),
2518+
("JUMP", top, 11),
2519+
end,
2520+
("END_FOR", None, 12),
2521+
("POP_ITER", None, 13),
2522+
("LOAD_CONST", 0, 14),
2523+
("RETURN_VALUE", None, 15),
2524+
]
2525+
after = [
2526+
("LOAD_CONST", 1, 7),
2527+
("GET_ITER", 0, 8),
2528+
top := self.Label(),
2529+
("FOR_ITER", end := self.Label(), 9),
2530+
("STORE_FAST", 0, 10),
2531+
("JUMP", top, 11),
2532+
end,
2533+
("END_FOR", None, 12),
2534+
("POP_ITER", None, 13),
2535+
("LOAD_CONST", 0, 14),
2536+
("RETURN_VALUE", None, 15),
2537+
]
2538+
self.cfg_optimization_test(before, after, consts=["test"],
2539+
expected_consts=["test", frozenset({1, 2, 3})])
2540+
2541+
def test_fold_constant_list_to_tuple_for_iter(self):
2542+
INTRINSIC_LIST_TO_TUPLE = 6
2543+
before = [
2544+
("BUILD_LIST", 0, 1),
2545+
("LOAD_SMALL_INT", 1, 2), ("LIST_APPEND", 1, 3),
2546+
("LOAD_SMALL_INT", 2, 4), ("LIST_APPEND", 1, 5),
2547+
("LOAD_SMALL_INT", 3, 6), ("LIST_APPEND", 1, 7),
2548+
("CALL_INTRINSIC_1", INTRINSIC_LIST_TO_TUPLE, 8),
2549+
("GET_ITER", 0, 9),
2550+
top := self.Label(),
2551+
("FOR_ITER", end := self.Label(), 10),
2552+
("STORE_FAST", 0, 11),
2553+
("JUMP", top, 12),
2554+
end,
2555+
("END_FOR", None, 13),
2556+
("POP_ITER", None, 14),
2557+
("LOAD_CONST", 0, 15),
2558+
("RETURN_VALUE", None, 16),
2559+
]
2560+
after = [
2561+
("LOAD_CONST", 1, 8),
2562+
("GET_ITER", 0, 9),
2563+
top := self.Label(),
2564+
("FOR_ITER", end := self.Label(), 10),
2565+
("STORE_FAST", 0, 11),
2566+
("JUMP", top, 12),
2567+
end,
2568+
("END_FOR", None, 13),
2569+
("POP_ITER", None, 14),
2570+
("LOAD_CONST", 0, 15),
2571+
("RETURN_VALUE", None, 16),
2572+
]
2573+
self.cfg_optimization_test(before, after, consts=["test"],
2574+
expected_consts=["test", (1, 2, 3)])
2575+
2576+
def test_fold_constant_big_list_contains_op(self):
2577+
# x in [c1, c2, ..., cN] (N > 30) should fold to LOAD_CONST tuple
2578+
before = [
2579+
("LOAD_FAST", 0, 1),
2580+
("BUILD_LIST", 0, 2),
2581+
("LOAD_SMALL_INT", 1, 3), ("LIST_APPEND", 1, 4),
2582+
("LOAD_SMALL_INT", 2, 5), ("LIST_APPEND", 1, 6),
2583+
("LOAD_SMALL_INT", 3, 7), ("LIST_APPEND", 1, 8),
2584+
("CONTAINS_OP", 0, 9),
2585+
("RETURN_VALUE", None, 10),
2586+
]
2587+
after = [
2588+
("LOAD_FAST_BORROW", 0, 1),
2589+
("LOAD_CONST", 1, 8),
2590+
("CONTAINS_OP", 0, 9),
2591+
("RETURN_VALUE", None, 10),
2592+
]
2593+
self.cfg_optimization_test(before, after, consts=[None],
2594+
expected_consts=[None, (1, 2, 3)])
2595+
2596+
def test_fold_constant_big_set_contains_op(self):
2597+
# x in {c1, c2, ..., cN} (N > 30) should fold to LOAD_CONST frozenset
2598+
before = [
2599+
("LOAD_FAST", 0, 1),
2600+
("BUILD_SET", 0, 2),
2601+
("LOAD_SMALL_INT", 1, 3), ("SET_ADD", 1, 4),
2602+
("LOAD_SMALL_INT", 2, 5), ("SET_ADD", 1, 6),
2603+
("LOAD_SMALL_INT", 3, 7), ("SET_ADD", 1, 8),
2604+
("CONTAINS_OP", 0, 9),
2605+
("RETURN_VALUE", None, 10),
2606+
]
2607+
after = [
2608+
("LOAD_FAST_BORROW", 0, 1),
2609+
("LOAD_CONST", 1, 8),
2610+
("CONTAINS_OP", 0, 9),
2611+
("RETURN_VALUE", None, 10),
2612+
]
2613+
self.cfg_optimization_test(before, after, consts=[None],
2614+
expected_consts=[None, frozenset({1, 2, 3})])
2615+
2616+
def test_no_fold_big_list_for_iter_with_non_const(self):
2617+
same = [
2618+
("BUILD_LIST", 0, 1),
2619+
("LOAD_SMALL_INT", 1, 2), ("LIST_APPEND", 1, 3),
2620+
("LOAD_FAST_BORROW", 0, 4), ("LIST_APPEND", 1, 5),
2621+
("LOAD_SMALL_INT", 3, 6), ("LIST_APPEND", 1, 7),
2622+
("GET_ITER", 0, 8),
2623+
top := self.Label(),
2624+
("FOR_ITER", end := self.Label(), 9),
2625+
("STORE_FAST", 1, 10),
2626+
("JUMP", top, 11),
2627+
end,
2628+
("END_FOR", None, 12),
2629+
("POP_ITER", None, 13),
2630+
("LOAD_CONST", 0, 14),
2631+
("RETURN_VALUE", None, 15),
2632+
]
2633+
self.cfg_optimization_test(same, same, consts=["test"])
2634+
24732635

24742636
class OptimizeLoadFastTestCase(DirectCfgOptimizerTests):
24752637
def make_bb(self, insts):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fold large constant list and set literals used as the iterable of a
2+
:keyword:`for` loop or ``in``/``not in`` test into a constant
3+
:class:`tuple` or :class:`frozenset`, restoring an optimization
4+
previously done by the AST optimizer that was lost when constant
5+
folding moved to the CFG.

Python/flowgraph.c

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1563,34 +1563,48 @@ fold_tuple_of_constants(basicblock *bb, int i, PyObject *consts,
15631563
}
15641564

15651565
/* Replace:
1566-
BUILD_LIST 0
1566+
BUILD_LIST/BUILD_SET 0
15671567
LOAD_CONST c1
1568-
LIST_APPEND 1
1568+
LIST_APPEND/SET_ADD 1
15691569
LOAD_CONST c2
1570-
LIST_APPEND 1
1570+
LIST_APPEND/SET_ADD 1
15711571
...
15721572
LOAD_CONST cN
1573-
LIST_APPEND 1
1574-
CALL_INTRINSIC_1 INTRINSIC_LIST_TO_TUPLE
1573+
LIST_APPEND/SET_ADD 1
1574+
[CALL_INTRINSIC_1 INTRINSIC_LIST_TO_TUPLE] <-- optional
15751575
with:
15761576
LOAD_CONST (c1, c2, ... cN)
1577+
The instruction at `i` is either the LIST_TO_TUPLE intrinsic (so the
1578+
immediately preceding non-NOP instruction is expected to be a
1579+
LIST_APPEND, and only the BUILD_LIST/LIST_APPEND form is considered),
1580+
or the trailing LIST_APPEND or SET_ADD itself, in which case the
1581+
matching BUILD_LIST/BUILD_SET start is selected from its opcode, and
1582+
for sets the result is wrapped in a frozenset.
15771583
*/
15781584
static int
1579-
fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
1580-
PyObject *consts, PyObject *const_cache,
1581-
_Py_hashtable_t *consts_index)
1585+
fold_constant_seq_into_load_const(basicblock *bb, int i,
1586+
PyObject *consts, PyObject *const_cache,
1587+
_Py_hashtable_t *consts_index)
15821588
{
15831589
assert(PyDict_CheckExact(const_cache));
15841590
assert(PyList_CheckExact(consts));
15851591
assert(i >= 0);
15861592
assert(i < bb->b_iused);
15871593

1588-
cfg_instr *intrinsic = &bb->b_instr[i];
1589-
assert(intrinsic->i_opcode == CALL_INTRINSIC_1);
1590-
assert(intrinsic->i_oparg == INTRINSIC_LIST_TO_TUPLE);
1591-
1594+
cfg_instr *target = &bb->b_instr[i];
1595+
assert(target->i_opcode == LIST_APPEND || target->i_opcode == SET_ADD ||
1596+
(target->i_opcode == CALL_INTRINSIC_1 &&
1597+
target->i_oparg == INTRINSIC_LIST_TO_TUPLE));
1598+
bool expected_append = target->i_opcode == CALL_INTRINSIC_1;
1599+
int append_op = expected_append ? LIST_APPEND : target->i_opcode;
1600+
assert(append_op == LIST_APPEND || append_op == SET_ADD);
1601+
int build_op = append_op == LIST_APPEND ? BUILD_LIST : BUILD_SET;
15921602
int consts_found = 0;
1593-
bool expect_append = true;
1603+
/* Walking backward from `i`, we expect LIST_APPEND/SET_ADD and
1604+
LOAD_CONST to alternate. If `i` is the trailing LIST_TO_TUPLE
1605+
intrinsic, the next instruction back is an APPEND. If `i` is the
1606+
trailing APPEND itself, the next instruction back is a LOAD_CONST. */
1607+
bool expect_append = expected_append;
15941608

15951609
for (int pos = i - 1; pos >= 0; pos--) {
15961610
cfg_instr *instr = &bb->b_instr[pos];
@@ -1601,7 +1615,7 @@ fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
16011615
continue;
16021616
}
16031617

1604-
if (opcode == BUILD_LIST && oparg == 0) {
1618+
if (opcode == build_op && oparg == 0) {
16051619
if (!expect_append) {
16061620
/* Not a sequence start. */
16071621
return SUCCESS;
@@ -1613,7 +1627,8 @@ fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
16131627
return ERROR;
16141628
}
16151629

1616-
for (int newpos = i - 1; newpos >= pos; newpos--) {
1630+
int newpos_start = expected_append ? i - 1 : i;
1631+
for (int newpos = newpos_start; newpos >= pos; newpos--) {
16171632
instr = &bb->b_instr[newpos];
16181633
if (instr->i_opcode == NOP) {
16191634
continue;
@@ -1630,11 +1645,20 @@ fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
16301645
nop_out(&instr, 1);
16311646
}
16321647
assert(consts_found == 0);
1633-
return instr_make_load_const(intrinsic, newconst, consts, const_cache, consts_index);
1648+
1649+
if (build_op == BUILD_SET) {
1650+
PyObject *frozen = PyFrozenSet_New(newconst);
1651+
Py_DECREF(newconst);
1652+
if (frozen == NULL) {
1653+
return ERROR;
1654+
}
1655+
newconst = frozen;
1656+
}
1657+
return instr_make_load_const(target, newconst, consts, const_cache, consts_index);
16341658
}
16351659

16361660
if (expect_append) {
1637-
if (opcode != LIST_APPEND || oparg != 1) {
1661+
if (opcode != append_op || oparg != 1) {
16381662
return SUCCESS;
16391663
}
16401664
}
@@ -2573,17 +2597,22 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts,
25732597
break;
25742598
case CALL_INTRINSIC_1:
25752599
if (oparg == INTRINSIC_LIST_TO_TUPLE) {
2576-
if (nextop == GET_ITER) {
2600+
RETURN_IF_ERROR(fold_constant_seq_into_load_const(bb, i, consts, const_cache, consts_index));
2601+
if (inst->i_opcode == CALL_INTRINSIC_1 && nextop == GET_ITER) {
25772602
INSTR_SET_OP0(inst, NOP);
25782603
}
2579-
else {
2580-
RETURN_IF_ERROR(fold_constant_intrinsic_list_to_tuple(bb, i, consts, const_cache, consts_index));
2581-
}
25822604
}
25832605
else if (oparg == INTRINSIC_UNARY_POSITIVE) {
25842606
RETURN_IF_ERROR(fold_const_unaryop(bb, i, consts, const_cache, consts_index));
25852607
}
25862608
break;
2609+
case LIST_APPEND:
2610+
case SET_ADD:
2611+
if (oparg == 1 && (nextop == GET_ITER || nextop == CONTAINS_OP)) {
2612+
RETURN_IF_ERROR(fold_constant_seq_into_load_const(
2613+
bb, i, consts, const_cache, consts_index));
2614+
}
2615+
break;
25872616
case BINARY_OP:
25882617
RETURN_IF_ERROR(fold_const_binop(bb, i, consts, const_cache, consts_index));
25892618
break;

0 commit comments

Comments
 (0)