Skip to content

Commit 8b4c0ef

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Add new flow graph optimization to fold to constants
Summary: 3.16 added a new flow graph optimization. This catches our compiler up. PythonLib/cinderx/compiler/flow_graph_optimizer.py - Generalized fold_constant_intrinsic_list_to_tuple → fold_constant_seq_into_load_const, mirroring upstream: the target may be the LIST_TO_TUPLE intrinsic or a trailing LIST_APPEND/SET_ADD, with sets folding to a frozenset. Behavior for 3.14/3.15 is unchanged (their only caller still passes the intrinsic). - Added FlowGraphOptimizer316 with the new LIST_APPEND/SET_ADD handler and the reordered CALL_INTRINSIC_1 handler. PythonLib/cinderx/compiler/pyassem.py — PyFlowGraph316.flow_graph_optimizer = FlowGraphOptimizer316. PythonLib/test_cinderx/test_compiler/test_optimizer.py — three version-gated regression tests (big const list iteration, big const set membership, and a negative case ensuring a list that escapes is still built as a list). Two of the three fail without the fix; 3.12 is skipped since it folds big constant sequences in codegen instead. Reviewed By: yoney Differential Revision: D116650708 fbshipit-source-id: 6b2626b6aaa5ba002d18ee8c986068d5731f3cb4
1 parent 5e7c015 commit 8b4c0ef

3 files changed

Lines changed: 134 additions & 17 deletions

File tree

cinderx/PythonLib/cinderx/compiler/flow_graph_optimizer.py

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,43 +1048,65 @@ def optimize_unary_not(
10481048
assert isinstance(self, FlowGraphOptimizer314)
10491049
self.optimize_one_unary(instr_index, instr, block, operator.not_)
10501050

1051-
def fold_constant_intrinsic_list_to_tuple(
1052-
self, block: Block, instr_index: int
1053-
) -> None:
1054-
consts_found = 0
1055-
expect_append = True
1051+
def fold_constant_seq_into_load_const(self, block: Block, instr_index: int) -> None:
1052+
"""Replace:
1053+
BUILD_LIST/BUILD_SET 0
1054+
LOAD_CONST c1
1055+
LIST_APPEND/SET_ADD 1
1056+
...
1057+
LOAD_CONST cN
1058+
LIST_APPEND/SET_ADD 1
1059+
[CALL_INTRINSIC_1 INTRINSIC_LIST_TO_TUPLE] <-- optional
1060+
with:
1061+
LOAD_CONST (c1, c2, ... cN)
1062+
1063+
The instruction at `instr_index` is either the LIST_TO_TUPLE intrinsic
1064+
(so only the BUILD_LIST/LIST_APPEND form is considered), or the trailing
1065+
LIST_APPEND or SET_ADD itself, in which case the matching
1066+
BUILD_LIST/BUILD_SET start is selected from its opcode and sets are
1067+
folded into a frozenset.
1068+
"""
1069+
target = block.insts[instr_index]
1070+
expected_append = target.opname == "CALL_INTRINSIC_1"
1071+
append_op = "LIST_APPEND" if expected_append else target.opname
1072+
build_op = "BUILD_LIST" if append_op == "LIST_APPEND" else "BUILD_SET"
1073+
# Walking backwards, appends and the constants they push alternate. The
1074+
# intrinsic is preceded by an append, a trailing append by a constant.
1075+
expect_append = expected_append
10561076
for i in range(instr_index - 1, -1, -1):
10571077
instr = block.insts[i]
10581078
opcode = instr.opname
10591079
oparg = instr.oparg
10601080
if opcode == "NOP":
10611081
continue
10621082

1063-
if opcode == "BUILD_LIST" and oparg == 0:
1083+
if opcode == build_op and oparg == 0:
10641084
if not expect_append:
10651085
# Not a start sequence
10661086
return
10671087

10681088
# Sequence start, we are done.
10691089
consts = []
1070-
if opcode == "BUILD_LIST" and oparg == 0:
1071-
for newpos in range(instr_index - 1, i - 1, -1):
1072-
instr = block.insts[newpos]
1073-
if instr.opname in LOAD_CONST_INSTRS:
1074-
const = instr.oparg
1075-
consts.append(const)
1076-
instr.set_to_nop_no_loc()
1090+
start = instr_index - 1 if expected_append else instr_index
1091+
for newpos in range(start, i - 1, -1):
1092+
instr = block.insts[newpos]
1093+
if instr.opname in LOAD_CONST_INSTRS:
1094+
const = instr.oparg
1095+
consts.append(const)
1096+
instr.set_to_nop_no_loc()
10771097

10781098
consts.reverse()
1079-
self.make_load_const(block.insts[instr_index], tuple(consts))
1099+
newconst: object = (
1100+
frozenset(consts) if build_op == "BUILD_SET" else tuple(consts)
1101+
)
1102+
self.make_load_const(target, newconst)
10801103
return
10811104

10821105
if expect_append:
1083-
if opcode != "LIST_APPEND" or oparg != 1:
1106+
if opcode != append_op or oparg != 1:
10841107
return
10851108
elif opcode not in LOAD_CONST_INSTRS:
10861109
return
1087-
consts_found += 1
10881110
expect_append = not expect_append
10891111

10901112
def optimize_call_intrinsic_1(
@@ -1101,7 +1123,7 @@ def optimize_call_intrinsic_1(
11011123
if next_instr is not None and next_instr.opname == "GET_ITER":
11021124
instr.set_to_nop()
11031125
else:
1104-
self.fold_constant_intrinsic_list_to_tuple(block, instr_index)
1126+
self.fold_constant_seq_into_load_const(block, instr_index)
11051127
if intrins == "INTRINSIC_UNARY_POSITIVE":
11061128
# pyrefly: ignore [bad-argument-type]
11071129
self.optimize_one_unary(instr_index, instr, block, operator.pos)
@@ -1154,6 +1176,62 @@ def optimize_basic_block(self, block: Block) -> None:
11541176
i += 1
11551177

11561178

1179+
class FlowGraphOptimizer316(FlowGraphOptimizer314):
1180+
"""Python 3.16-specific optimizations."""
1181+
1182+
def optimize_call_intrinsic_1(
1183+
self: FlowGraphOptimizer,
1184+
instr_index: int,
1185+
instr: Instruction,
1186+
next_instr: Instruction | None,
1187+
target: Instruction | None,
1188+
block: Block,
1189+
) -> int | None:
1190+
assert isinstance(self, FlowGraphOptimizer316)
1191+
intrins = INTRINSIC_1[instr.ioparg]
1192+
if intrins == "INTRINSIC_LIST_TO_TUPLE":
1193+
# Unlike 3.14/3.15, folding is attempted even when iterating, so a
1194+
# big constant tuple becomes a single LOAD_CONST instead of staying
1195+
# a list build. The intrinsic is only dropped if folding didn't
1196+
# already rewrite it.
1197+
self.fold_constant_seq_into_load_const(block, instr_index)
1198+
if (
1199+
instr.opname == "CALL_INTRINSIC_1"
1200+
and next_instr is not None
1201+
and next_instr.opname == "GET_ITER"
1202+
):
1203+
instr.set_to_nop()
1204+
if intrins == "INTRINSIC_UNARY_POSITIVE":
1205+
# pyrefly: ignore [bad-argument-type]
1206+
self.optimize_one_unary(instr_index, instr, block, operator.pos)
1207+
1208+
def optimize_list_append_set_add(
1209+
self: FlowGraphOptimizer,
1210+
instr_index: int,
1211+
instr: Instruction,
1212+
next_instr: Instruction | None,
1213+
target: Instruction | None,
1214+
block: Block,
1215+
) -> int | None:
1216+
assert isinstance(self, FlowGraphOptimizer316)
1217+
# Sequences too big for optimize_lists_and_sets are built with repeated
1218+
# appends. When they're only iterated over or tested for membership a
1219+
# constant tuple/frozenset is a suitable replacement.
1220+
if (
1221+
instr.oparg == 1
1222+
and next_instr is not None
1223+
and next_instr.opname in ("GET_ITER", "CONTAINS_OP")
1224+
):
1225+
self.fold_constant_seq_into_load_const(block, instr_index)
1226+
1227+
handlers: dict[str, Handler] = {
1228+
**FlowGraphOptimizer314.handlers,
1229+
"CALL_INTRINSIC_1": optimize_call_intrinsic_1,
1230+
"LIST_APPEND": optimize_list_append_set_add,
1231+
"SET_ADD": optimize_list_append_set_add,
1232+
}
1233+
1234+
11571235
class FlowGraphConstOptimizer314(BaseFlowGraphOptimizer314):
11581236
def opt_load_const(
11591237
self: FlowGraphOptimizer,

cinderx/PythonLib/cinderx/compiler/pyassem.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
FlowGraphOptimizer,
5656
FlowGraphOptimizer312,
5757
FlowGraphOptimizer314,
58+
FlowGraphOptimizer316,
5859
)
5960
from .opcode_cinder import opcode as cinder_opcode
6061
from .opcodebase import Opcode
@@ -3305,6 +3306,7 @@ def propagate_line_numbers(self) -> None:
33053306

33063307

33073308
class PyFlowGraph316(PyFlowGraph315):
3309+
flow_graph_optimizer = FlowGraphOptimizer316
33083310
flow_graph_const_optimizer = FlowGraphConstOptimizer316
33093311

33103312
def convert_load_const_to_load_common_constant(self) -> None:

cinderx/PythonLib/test_cinderx/test_compiler/test_optimizer.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030

3131
from .common import CompilerTest
3232

33+
# A constant sequence long enough to exceed STACK_USE_GUIDELINE, so it is built
34+
# with repeated appends rather than a single BUILD_LIST/BUILD_SET.
35+
BIG_CONST_TUPLE: tuple[int, ...] = tuple(range(100, 140))
36+
BIG_CONST_ELTS: str = ", ".join(str(i) for i in BIG_CONST_TUPLE)
37+
3338

3439
class AstOptimizerTests(CompilerTest):
3540
class _Comparer:
@@ -483,6 +488,38 @@ def test_frozenset_call_optimization(self) -> None:
483488
else:
484489
self.assertNotInGraph(graph, "CALL_INTRINSIC_1")
485490

491+
@passIf(sys.version_info < (3, 14), "3.12 folds big constant sequences in codegen")
492+
def test_big_const_list_iteration(self) -> None:
493+
# Sequences over STACK_USE_GUIDELINE elements are built with repeated
494+
# appends. 3.16 folds such a chain into a constant tuple when the
495+
# result is only iterated over; earlier versions keep the build.
496+
graph = self.to_graph(f"for x in [{BIG_CONST_ELTS}]: pass")
497+
if sys.version_info >= (3, 16):
498+
self.assertInGraph(graph, "LOAD_CONST", BIG_CONST_TUPLE)
499+
self.assertNotInGraph(graph, "BUILD_LIST")
500+
else:
501+
self.assertInGraph(graph, "BUILD_LIST", 0)
502+
self.assertNotInGraph(graph, "LOAD_CONST", BIG_CONST_TUPLE)
503+
504+
@passIf(sys.version_info < (3, 14), "3.12 folds big constant sequences in codegen")
505+
def test_big_const_set_membership(self) -> None:
506+
# Same as above for sets, which fold into a frozenset.
507+
graph = self.to_graph(f"y = x in {{{BIG_CONST_ELTS}}}")
508+
if sys.version_info >= (3, 16):
509+
self.assertInGraph(graph, "LOAD_CONST", frozenset(BIG_CONST_TUPLE))
510+
self.assertNotInGraph(graph, "BUILD_SET")
511+
else:
512+
self.assertInGraph(graph, "BUILD_SET", 0)
513+
self.assertNotInGraph(graph, "LOAD_CONST", frozenset(BIG_CONST_TUPLE))
514+
515+
@passIf(sys.version_info < (3, 14), "3.12 folds big constant sequences in codegen")
516+
def test_big_const_list_not_folded_when_used_as_a_list(self) -> None:
517+
# The fold is only valid when the sequence is consumed by GET_ITER or
518+
# CONTAINS_OP; a list that escapes must still be built as a list.
519+
graph = self.to_graph(f"x = [{BIG_CONST_ELTS}]")
520+
self.assertInGraph(graph, "BUILD_LIST", 0)
521+
self.assertNotInGraph(graph, "LOAD_CONST", BIG_CONST_TUPLE)
522+
486523

487524
class _FakeInstr:
488525
__slots__ = ("opname", "oparg", "ioparg")

0 commit comments

Comments
 (0)