Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some static analysis in bytecode rewriting #519

Draft
wants to merge 71 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
b90d69c
Initial scaffolding
tekknolagi Nov 15, 2023
183593e
WIP: Run until fixpoint
tekknolagi Nov 15, 2023
c0ec58c
WIP: Fix jump offsets for jump instructions
tekknolagi Nov 16, 2023
d7b8cb1
Log reason for bailout
tekknolagi Nov 16, 2023
724cba3
Remove commented code
tekknolagi Nov 16, 2023
759a30f
Remember to call meet
tekknolagi Nov 16, 2023
c70bf20
Expose a builtin function for analyzing bytecode
tekknolagi Nov 16, 2023
fe0a261
nit
tekknolagi Nov 16, 2023
71a2d36
WIP: everything is broken
tekknolagi Nov 16, 2023
8c6fdae
wip what is going on
tekknolagi Nov 28, 2023
66dc669
Something looks more promising
tekknolagi Nov 28, 2023
9842fd6
Seems to work...!!!
tekknolagi Nov 28, 2023
96f0b8c
Add safeguard
tekknolagi Nov 28, 2023
ca9e16f
Cleanup
tekknolagi Nov 28, 2023
1d94d88
Remove print for tests
tekknolagi Nov 28, 2023
c57b9e1
Rewrite opcodes
tekknolagi Nov 28, 2023
3fd9209
Bail out in some tests
tekknolagi Nov 28, 2023
3866d67
Move bailout code to general analysis function
tekknolagi Nov 28, 2023
3501d88
Pass in edges to all analyses
tekknolagi Nov 28, 2023
4e153ea
Add probes for attempts/success
tekknolagi Nov 28, 2023
5d15d67
Support FOR_ITER
tekknolagi Nov 28, 2023
4b9a778
Support RAISE_VARARGS without try/except handlers
tekknolagi Nov 28, 2023
e4839be
Add RETURN_VALUE to test
tekknolagi Nov 28, 2023
2100d00
Add a DCHECK for the last op being RETURN_VALUE
tekknolagi Nov 28, 2023
1f3a350
Add runUntilFixpoint
tekknolagi Nov 28, 2023
fa29ac1
clang-format
tekknolagi Nov 28, 2023
88dc5c0
Gather stats about analysis
tekknolagi Nov 28, 2023
b3fd31e
Add TODO
tekknolagi Nov 28, 2023
c1e8bee
Return num_iterations from runUntilFixpoint
tekknolagi Nov 29, 2023
2c66ebe
Inline runDefiniteAssignmentOpcode
tekknolagi Nov 29, 2023
d364dfc
Checkpoint: rewrite with lattice structures
tekknolagi Nov 30, 2023
bd14e0e
Cleanup
tekknolagi Nov 30, 2023
e2e5a34
Move meet to Locals
tekknolagi Nov 30, 2023
4ccce2c
Use better vector constructor
tekknolagi Nov 30, 2023
33bd90d
Replace Vector with std::vector
tekknolagi Nov 30, 2023
c7e08fe
Bump max num iterations for experiments with live variable analysis
tekknolagi Nov 30, 2023
eebb8dd
Add live variable analysis
tekknolagi Nov 30, 2023
4f600e0
Add TODO
tekknolagi Dec 6, 2023
86e5934
Consider more opcodes and use bitset instead
tekknolagi Nov 30, 2023
ea04194
Clang format
tekknolagi Dec 12, 2023
04f7418
Cleanup
tekknolagi Dec 12, 2023
0d04642
Mark unimplemented
tekknolagi Dec 12, 2023
9f718a8
Remove unused fancy lattice
tekknolagi Dec 12, 2023
34c9b0e
Use update function to detect changes
tekknolagi Dec 12, 2023
f3971b8
Cleanup
tekknolagi Dec 12, 2023
1979859
Cleanup
tekknolagi Dec 12, 2023
ab64701
Cleanup
tekknolagi Dec 12, 2023
8471beb
Add UNCHECKED variants to liveness
tekknolagi Dec 12, 2023
5875813
Add some more sanity checks
tekknolagi Dec 12, 2023
54a8eca
Add TODO
tekknolagi Dec 12, 2023
ce4b91c
Add TODO
tekknolagi Dec 12, 2023
590bfea
Re-enable Python version of definite assignnment
tekknolagi Dec 12, 2023
0618ab3
WIP
tekknolagi Dec 13, 2023
2c23236
Cleanup
tekknolagi Dec 13, 2023
8a2727c
Remove unused C++ code now...
tekknolagi Dec 13, 2023
a73c5fc
Add num_iterations
tekknolagi Dec 13, 2023
202c982
Remove _analyze_bytecode
tekknolagi Dec 13, 2023
b7a0120
Do liveness first
tekknolagi Dec 13, 2023
ecbc526
Remove print
tekknolagi Dec 13, 2023
aa3d987
nit
tekknolagi Dec 13, 2023
a6d234e
nit
tekknolagi Dec 13, 2023
a453072
nit
tekknolagi Dec 13, 2023
09c0da1
Fix DELETE_FAST handling
tekknolagi Dec 13, 2023
099efe8
Add tests for live variable analysis
tekknolagi Dec 13, 2023
0c57c01
Add another test
tekknolagi Dec 13, 2023
12a9310
Abort if calling locals()
tekknolagi Dec 13, 2023
5c8097e
Fix C++ test
tekknolagi Dec 13, 2023
4bab890
Fix test
tekknolagi Dec 13, 2023
90b2e9e
Lint
tekknolagi Dec 13, 2023
075dbe0
Add a test
tekknolagi Dec 13, 2023
b0fa910
WIP: disable both optimizations to get benchmark results
tekknolagi Dec 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 244 additions & 1 deletion library/_compile_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,6 @@ def foo(x):
"""\
LOAD_FAST_REVERSE_UNCHECKED x
STORE_FAST_REVERSE y
DELETE_FAST x
LOAD_FAST_REVERSE_UNCHECKED y
RETURN_VALUE
""",
Expand Down Expand Up @@ -1485,6 +1484,250 @@ async def foo():

# TODO(emacs): Test with (multiple context managers)

def test_dead_local_store_is_removed(self):
source = """
def foo():
x = 123
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_CONST 123
POP_TOP
LOAD_CONST None
RETURN_VALUE
""",
)
self.assertEqual(func(), None)

def test_dead_local_store_in_one_branch_is_removed(self):
source = """
def foo(cond):
if cond:
x = 123
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_FAST_REVERSE_UNCHECKED cond
POP_JUMP_IF_FALSE 8
LOAD_CONST 123
POP_TOP
LOAD_CONST None
RETURN_VALUE
""",
)
self.assertEqual(func(True), None)

def test_dead_local_store_in_all_branches_is_removed(self):
source = """
def foo(cond):
if cond:
x = 123
else:
x = 456
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_FAST_REVERSE_UNCHECKED cond
POP_JUMP_IF_FALSE 10
LOAD_CONST 123
POP_TOP
JUMP_FORWARD 4
LOAD_CONST 456
POP_TOP
LOAD_CONST None
RETURN_VALUE
""",
)
self.assertEqual(func(True), None)

def test_local_store_used_in_same_branch_removed_in_other_branch(self):
source = """
def foo(cond):
if cond:
x = 123
f(x)
else:
x = 456
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_FAST_REVERSE_UNCHECKED cond
POP_JUMP_IF_FALSE 18
LOAD_CONST 123
STORE_FAST_REVERSE x
LOAD_GLOBAL f
LOAD_FAST_REVERSE_UNCHECKED x
CALL_FUNCTION 1
POP_TOP
JUMP_FORWARD 4
LOAD_CONST 456
POP_TOP
LOAD_CONST None
RETURN_VALUE
""",
)
self.assertEqual(func(False), None)

def test_store_before_store_removed(self):
source = """
def foo():
x = 2
x = 3
return x
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_CONST 2
POP_TOP
LOAD_CONST 3
STORE_FAST_REVERSE x
LOAD_FAST_REVERSE_UNCHECKED x
RETURN_VALUE
""",
)
self.assertEqual(func(), 3)

def test_store_before_del_removed(self):
source = """
def foo():
x = 2
del x
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_CONST 2
POP_TOP
LOAD_CONST None
RETURN_VALUE
""",
)
self.assertEqual(func(), None)

@unittest.skip("TODO: Figure out how to leave one DELETE_FAST")
def test_del_before_del_leaves_one_del(self):
source = """
def foo():
x = 2
del x
del x
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_CONST 2
POP_TOP
DELETE_FAST x
LOAD_CONST None
RETURN_VALUE
""",
)
self.assertEqual(func(), None)

def test_store_before_del_and_use_removed(self):
# TODO(max): Remove the unused STORE_FAST and DELETE_FAST
source = """
def foo():
x = 2
del x
return x
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
DELETE_FAST_REVERSE_UNCHECKED x
LOAD_CONST 2
STORE_FAST_REVERSE x
DELETE_FAST x
LOAD_FAST x
RETURN_VALUE
""",
)
with self.assertRaises(UnboundLocalError):
func()

@unittest.skip("TODO(max): Figure out why this test is failing")
def test_dead_local_store_used_in_other_branch_is_removed(self):
source = """
def foo(cond):
if cond:
x = 123
else:
f(x)
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
DELETE_FAST_REVERSE_UNCHECKED x
LOAD_FAST_REVERSE_UNCHECKED cond
POP_JUMP_IF_FALSE 12
LOAD_CONST 123
POP_TOP
JUMP_FORWARD 8
LOAD_GLOBAL f
LOAD_FAST x
CALL_FUNCTION 1
POP_TOP
LOAD_CONST None
RETURN_VALUE
""",
)
self.assertEqual(func(True), None)

def test_dead_store_not_removed_if_calling_locals_function(self):
source = """
def foo():
x = 123
return locals()
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_CONST 123
STORE_FAST_REVERSE x
LOAD_GLOBAL locals
CALL_FUNCTION 0
RETURN_VALUE
""")
self.assertEqual(func(), {"x": 123})

def test_store_self_removes_last_store(self):
# TODO(max): See if we can remove the first store too
source = """
def foo():
x = 123
x = x
"""
func = compile_function(source, "foo")
self.assertEqual(
dis(func.__code__),
"""\
LOAD_CONST 123
STORE_FAST_REVERSE x
LOAD_FAST_REVERSE_UNCHECKED x
POP_TOP
LOAD_CONST None
RETURN_VALUE
""")
self.assertEqual(func(), None)

# TODO(max): Test loops


if __name__ == "__main__":
unittest.main()
104 changes: 103 additions & 1 deletion library/_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ class PyroFlowGraph(PyFlowGraph38):
opcode = opcodepyro.opcode

def optimizeLoadFast(self):
# TODO(max): Bail out early if gen/coro/asyncgen/itercoro?
# TODO(max): Bail out early if exception handling opcodes
# TODO(max): Make edges between all opcodes, not just basic blocks
# TODO(max): Profile number of iterations until fixpoint
blocks = self.getBlocksInOrder()
preds = tuple(set() for i in range(self.block_count))
for block in blocks:
Expand Down Expand Up @@ -277,10 +281,12 @@ def process_one_block(block, modify=False):
return True

changed = True
num_iterations = 0
while changed:
changed = False
for block in blocks:
changed |= process_one_block(block)
num_iterations += 1

for block in blocks:
process_one_block(block, modify=True)
Expand All @@ -296,8 +302,104 @@ def process_one_block(block, modify=False):
]
self.entry.insts = deletes + self.entry.insts

def getInstructions(self):
for block in self.getBlocksInOrder():
for instr in block.getInstructions():
yield instr

def optimizeDeadStores(self):
all_instrs = self.getInstructions()
if any(
instr.opname
in (
# Exception handling opcodes
"POP_BLOCK",
"SETUP_ASYNC_WITH",
"SETUP_FINALLY",
"SETUP_WITH",
"WITH_CLEANUP_START",
"YIELD_FROM",
"YIELD_VALUE",
"END_ASYNC_FOR",
)
for instr in all_instrs
):
return
if "locals" in self.names:
# This is hack to avoid optimizing away dead locals in the presence
# of one particular kind of call to locals().
return
# TODO(max): Bail out early if gen/coro/asyncgen/itercoro?
# TODO(max): Make edges between all opcodes, not just basic blocks
# TODO(max): Profile number of iterations until fixpoint
blocks = self.getBlocksInOrder()
preds = tuple(set() for i in range(self.block_count))
succs = tuple(set() for i in range(self.block_count))
for block in blocks:
for child in block.get_children():
if child is not None:
preds[child.bid].add(block.bid)
succs[block.bid].add(child.bid)

num_locals = len(self.varnames)
Top = 0
live_out = [Top] * self.block_count
total_locals = num_locals + len(self.cellvars) + len(self.freevars)

def reverse_local_idx(idx):
return total_locals - idx - 1

def meet(args):
result = Top
for arg in args:
result |= arg
return result

def process_one_block(block, modify=False):
bid = block.bid
if len(succs[bid]) == 0:
live = Top
else:
live = meet(live_out[succ] for succ in succs[bid])
for instr in reversed(block.getInstructions()):
if (
instr.opname == "DELETE_FAST"
and modify
and not (live & (1 << instr.ioparg))
):
instr.opname = "NOP"
instr.oparg = None
instr.ioparg = 0
live &= ~(1 << instr.ioparg)
if instr.opname == "LOAD_FAST" or instr.opname == "DELETE_FAST":
live |= 1 << instr.ioparg
elif instr.opname == "STORE_FAST":
if modify and not (live & (1 << instr.ioparg)):
instr.opname = "POP_TOP"
instr.oparg = None
instr.ioparg = 0
live &= ~(1 << instr.ioparg)
if live == live_out[bid]:
return False
live_out[bid] = live
return True

changed = True
num_iterations = 0
while changed:
changed = False
for block in blocks:
changed |= process_one_block(block)
num_iterations += 1

for block in blocks:
process_one_block(block, modify=True)

def getCode(self):
self.optimizeLoadFast()
# Do this first; it can't (yet?) handle LOAD_FAST_REVERSE_UNCHECKED et
# al.
# self.optimizeDeadStores()
# self.optimizeLoadFast()
return super().getCode()


Expand Down
Loading
Loading