Skip to content

Commit 6c7e480

Browse files
authored
[mypyc] Remove unused labels and gotos (#15244)
This PR removes labels which are never jumped to, as well as removing gotos that jump directly to the next label (and removes the now redundant label). I had a small issue with the `GetAttr` op code as it would insert C code after the end of the branch which could not be detected at the IR level, as seen in mypyc/mypyc#600 (comment) . Here are some basic stats on the file sizes before and after this PR: Building with `MYPY_OPT_LEVEL=0`, `MYPY_DEBUG_LEVEL=1`: | Branch | `.so` size | `.c` size | `.c` lines | |----------|-----------------|-----------------|-----------------| | `master` | 43.9 MB | 79.7 MB | 2290675 | | This PR | 43.6 MB (-0.6%) | 78.6 MB (-1.4%) | 2179967 (-4.8%) | Building with `MYPY_OPT_LEVEL=3`, `MYPY_DEBUG_LEVEL=0`: | Branch | `.so` size | `.c` size | `.c` lines | |----------|------------|-----------|------------| | `master` | 26.5 MB | 79.7 MB | 2291316 | | This PR | 26.5 MB | 78.7 MB | 2179701 | (I don't know why the number of lines changes between optimization levels. It's only a couple hundred lines, perhaps Mypyc is emitting different code depending on optimization level?) Unoptimized builds seem to benefit the most, while optimized builds look pretty much the same. I didn't check to see if they are *exactly* the same binary, but they are probably close if not the same. The biggest win is the drop in the number of lines C code being generated. Closes mypyc/mypyc#600
1 parent 2ede35f commit 6c7e480

File tree

4 files changed

+35
-11
lines changed

4 files changed

+35
-11
lines changed

mypyc/codegen/emit.py

+3
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ def emit_label(self, label: BasicBlock | str) -> None:
229229
if isinstance(label, str):
230230
text = label
231231
else:
232+
if label.label == 0 or not label.referenced:
233+
return
234+
232235
text = self.label(label)
233236
# Extra semicolon prevents an error when the next line declares a tempvar
234237
self.fragments.append(f"{text}: ;\n")

mypyc/codegen/emitfunc.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
CallC,
2424
Cast,
2525
ComparisonOp,
26+
ControlOp,
2627
DecRef,
2728
Extend,
2829
Float,
@@ -123,6 +124,28 @@ def generate_native_function(
123124
for i, block in enumerate(blocks):
124125
block.label = i
125126

127+
# Find blocks that are never jumped to or are only jumped to from the
128+
# block directly above it. This allows for more labels and gotos to be
129+
# eliminated during code generation.
130+
for block in fn.blocks:
131+
terminator = block.terminator
132+
assert isinstance(terminator, ControlOp)
133+
134+
for target in terminator.targets():
135+
is_next_block = target.label == block.label + 1
136+
137+
# Always emit labels for GetAttr error checks since the emit code that
138+
# generates them will add instructions between the branch and the
139+
# next label, causing the label to be wrongly removed. A better
140+
# solution would be to change the IR so that it adds a basic block
141+
# inbetween the calls.
142+
is_problematic_op = isinstance(terminator, Branch) and any(
143+
isinstance(s, GetAttr) for s in terminator.sources()
144+
)
145+
146+
if not is_next_block or is_problematic_op:
147+
fn.blocks[target.label].referenced = True
148+
126149
common = frequently_executed_blocks(fn.blocks[0])
127150

128151
for i in range(len(blocks)):
@@ -216,17 +239,20 @@ def visit_branch(self, op: Branch) -> None:
216239

217240
if false is self.next_block:
218241
if op.traceback_entry is None:
219-
self.emit_line(f"if ({cond}) goto {self.label(true)};")
242+
if true is not self.next_block:
243+
self.emit_line(f"if ({cond}) goto {self.label(true)};")
220244
else:
221245
self.emit_line(f"if ({cond}) {{")
222246
self.emit_traceback(op)
223247
self.emit_lines("goto %s;" % self.label(true), "}")
224248
else:
225249
self.emit_line(f"if ({cond}) {{")
226250
self.emit_traceback(op)
227-
self.emit_lines(
228-
"goto %s;" % self.label(true), "} else", " goto %s;" % self.label(false)
229-
)
251+
252+
if true is not self.next_block:
253+
self.emit_line("goto %s;" % self.label(true))
254+
255+
self.emit_lines("} else", " goto %s;" % self.label(false))
230256

231257
def visit_return(self, op: Return) -> None:
232258
value_str = self.reg(op.value)

mypyc/ir/ops.py

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def __init__(self, label: int = -1) -> None:
8181
self.label = label
8282
self.ops: list[Op] = []
8383
self.error_handler: BasicBlock | None = None
84+
self.referenced = False
8485

8586
@property
8687
def terminated(self) -> bool:

mypyc/test/test_emitfunc.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -894,12 +894,7 @@ def test_simple(self) -> None:
894894
generate_native_function(fn, emitter, "prog.py", "prog")
895895
result = emitter.fragments
896896
assert_string_arrays_equal(
897-
[
898-
"CPyTagged CPyDef_myfunc(CPyTagged cpy_r_arg) {\n",
899-
"CPyL0: ;\n",
900-
" return cpy_r_arg;\n",
901-
"}\n",
902-
],
897+
["CPyTagged CPyDef_myfunc(CPyTagged cpy_r_arg) {\n", " return cpy_r_arg;\n", "}\n"],
903898
result,
904899
msg="Generated code invalid",
905900
)
@@ -922,7 +917,6 @@ def test_register(self) -> None:
922917
[
923918
"PyObject *CPyDef_myfunc(CPyTagged cpy_r_arg) {\n",
924919
" CPyTagged cpy_r_r0;\n",
925-
"CPyL0: ;\n",
926920
" cpy_r_r0 = 10;\n",
927921
" CPy_Unreachable();\n",
928922
"}\n",

0 commit comments

Comments
 (0)