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

feat[venom]: improve liveness computation #4301

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
00165c8
feat[venom]: improve liveness computation
charles-cooper Oct 14, 2024
8619371
fix lint
charles-cooper Oct 14, 2024
87af6df
Merge branch 'master' into perf/liveness2
charles-cooper Oct 14, 2024
66bd73e
Merge branch 'master' into perf/liveness2
charles-cooper Oct 21, 2024
3f8ec64
remove dom tree from sccp pass
charles-cooper Oct 25, 2024
24f90a6
add dfs to cfg analysis. refactor out of dom tree analysis
charles-cooper Oct 25, 2024
9b8d3f3
fix from bad merge
charles-cooper Oct 25, 2024
d1951a0
fix dfs search - leaves first
charles-cooper Oct 25, 2024
d1e18d6
simplify cfg calculation
charles-cooper Oct 26, 2024
30fa633
fix a regression
charles-cooper Oct 26, 2024
67d1382
refactor: remove IRFunction.compute_reachability
charles-cooper Oct 26, 2024
a06d546
change cfg_out order
charles-cooper Oct 26, 2024
f198477
add a note
charles-cooper Oct 26, 2024
452c82e
some StackTooDeep are fixed
charles-cooper Oct 27, 2024
ee50088
fix sccp tests
charles-cooper Oct 27, 2024
5edb7fe
Merge branch 'master' into refactor/dfg-domtree
charles-cooper Oct 27, 2024
d3967f5
fix lint
charles-cooper Oct 27, 2024
240d9d2
use update instead of union
charles-cooper Oct 27, 2024
78da77c
skip unreachable bbs
charles-cooper Oct 27, 2024
2fe1397
refactor fix_phi_nodes
charles-cooper Oct 27, 2024
27518a5
remove useless __eq__ and __hash__ implementations
charles-cooper Oct 27, 2024
1b117c8
remove `IRBasicBlock.reachable`
charles-cooper Oct 29, 2024
6178526
Merge branch 'master' into refactor/dfg-domtree
charles-cooper Oct 29, 2024
df01ee8
Merge branch 'refactor/dfg-domtree' into perf/liveness2
charles-cooper Oct 29, 2024
3863ed2
fix from merge
charles-cooper Oct 29, 2024
8135181
Merge branch 'refactor/dfg-domtree' into perf/liveness2
charles-cooper Oct 29, 2024
5ec2cb2
Merge branch 'master' into perf/liveness2
charles-cooper Nov 12, 2024
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
4 changes: 0 additions & 4 deletions tests/functional/codegen/features/test_constructor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import pytest

from tests.evm_backends.base_env import _compile
from vyper.exceptions import StackTooDeep
from vyper.utils import method_id


Expand Down Expand Up @@ -169,7 +166,6 @@ def get_foo() -> uint256:
assert c.get_foo() == 39


@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_nested_dynamic_array_constructor_arg_2(env, get_contract):
code = """
foo: int128
Expand Down
2 changes: 0 additions & 2 deletions tests/functional/codegen/types/test_dynamic_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
CompilerPanic,
ImmutableViolation,
OverflowException,
StackTooDeep,
StateAccessViolation,
TypeMismatch,
)
Expand Down Expand Up @@ -737,7 +736,6 @@ def test_array_decimal_return3() -> DynArray[DynArray[decimal, 2], 2]:
]


@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_mult_list(get_contract):
code = """
nest3: DynArray[DynArray[DynArray[uint256, 2], 2], 2]
Expand Down
9 changes: 5 additions & 4 deletions tests/unit/compiler/venom/test_sccp.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ def test_cont_phi_case():
assert sccp.lattice[IRVariable("%2")].value == 32
assert sccp.lattice[IRVariable("%3")].value == 64
assert sccp.lattice[IRVariable("%4")].value == 96
assert sccp.lattice[IRVariable("%5", version=1)].value == 106
assert sccp.lattice[IRVariable("%5", version=2)] == LatticeEnum.BOTTOM
assert sccp.lattice[IRVariable("%5", version=2)].value == 106
assert sccp.lattice[IRVariable("%5", version=1)] == LatticeEnum.BOTTOM
assert sccp.lattice[IRVariable("%5")].value == 2


Expand Down Expand Up @@ -207,8 +207,9 @@ def test_cont_phi_const_case():
assert sccp.lattice[IRVariable("%2")].value == 32
assert sccp.lattice[IRVariable("%3")].value == 64
assert sccp.lattice[IRVariable("%4")].value == 96
assert sccp.lattice[IRVariable("%5", version=1)].value == 106
assert sccp.lattice[IRVariable("%5", version=2)].value == 97
# dependent on cfg traversal order
assert sccp.lattice[IRVariable("%5", version=2)].value == 106
assert sccp.lattice[IRVariable("%5", version=1)].value == 97
assert sccp.lattice[IRVariable("%5")].value == 2


Expand Down
47 changes: 34 additions & 13 deletions vyper/venom/analysis/cfg.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,62 @@
from typing import Iterator

from vyper.utils import OrderedSet
from vyper.venom.analysis import IRAnalysis
from vyper.venom.basicblock import CFG_ALTERING_INSTRUCTIONS
from vyper.venom.basicblock import CFG_ALTERING_INSTRUCTIONS, IRBasicBlock


class CFGAnalysis(IRAnalysis):
"""
Compute control flow graph information for each basic block in the function.
"""

_dfs: OrderedSet[IRBasicBlock]

def analyze(self) -> None:
fn = self.function
self._dfs = OrderedSet()

for bb in fn.get_basic_blocks():
bb.cfg_in = OrderedSet()
bb.cfg_out = OrderedSet()
bb.out_vars = OrderedSet()
bb.is_reachable = False

for bb in fn.get_basic_blocks():
assert len(bb.instructions) > 0, "Basic block should not be empty"
last_inst = bb.instructions[-1]
assert last_inst.is_bb_terminator, f"Last instruction should be a terminator {bb}"
assert bb.is_terminated

for inst in bb.instructions:
if inst.opcode in CFG_ALTERING_INSTRUCTIONS:
ops = inst.get_label_operands()
for op in ops:
fn.get_basic_block(op.value).add_cfg_in(bb)
term = bb.instructions[-1]
if term.opcode in CFG_ALTERING_INSTRUCTIONS:
ops = term.get_label_operands()
# order of cfg_out matters to performance!
for op in reversed(list(ops)):
next_bb = fn.get_basic_block(op.value)
bb.add_cfg_out(next_bb)
next_bb.add_cfg_in(bb)

# Fill in the "out" set for each basic block
for bb in fn.get_basic_blocks():
for in_bb in bb.cfg_in:
in_bb.add_cfg_out(bb)
self._compute_dfs_r(self.function.entry)

def _compute_dfs_r(self, bb):
if bb.is_reachable:
return
bb.is_reachable = True

for out_bb in bb.cfg_out:
self._compute_dfs_r(out_bb)

self._dfs.add(bb)

@property
def dfs_walk(self) -> Iterator[IRBasicBlock]:
return iter(self._dfs)

def invalidate(self):
from vyper.venom.analysis import DFGAnalysis, DominatorTreeAnalysis, LivenessAnalysis

self.analyses_cache.invalidate_analysis(DominatorTreeAnalysis)
self.analyses_cache.invalidate_analysis(LivenessAnalysis)

self._dfs = None

# be conservative - assume cfg invalidation invalidates dfg
self.analyses_cache.invalidate_analysis(DFGAnalysis)
29 changes: 9 additions & 20 deletions vyper/venom/analysis/dominators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from functools import cached_property

from vyper.exceptions import CompilerPanic
from vyper.utils import OrderedSet
from vyper.venom.analysis import CFGAnalysis, IRAnalysis
Expand All @@ -14,8 +16,6 @@ class DominatorTreeAnalysis(IRAnalysis):

fn: IRFunction
entry_block: IRBasicBlock
dfs_order: dict[IRBasicBlock, int]
dfs_walk: list[IRBasicBlock]
dominators: dict[IRBasicBlock, OrderedSet[IRBasicBlock]]
immediate_dominators: dict[IRBasicBlock, IRBasicBlock]
dominated: dict[IRBasicBlock, OrderedSet[IRBasicBlock]]
Expand All @@ -27,16 +27,13 @@ def analyze(self):
"""
self.fn = self.function
self.entry_block = self.fn.entry
self.dfs_order = {}
self.dfs_walk = []
self.dominators = {}
self.immediate_dominators = {}
self.dominated = {}
self.dominator_frontiers = {}

self.analyses_cache.request_analysis(CFGAnalysis)
self.cfg = self.analyses_cache.request_analysis(CFGAnalysis)

self._compute_dfs(self.entry_block, OrderedSet())
self._compute_dominators()
self._compute_idoms()
self._compute_df()
Expand Down Expand Up @@ -131,21 +128,13 @@ def _intersect(self, bb1, bb2):
bb2 = self.immediate_dominators[bb2]
return bb1

def _compute_dfs(self, entry: IRBasicBlock, visited):
"""
Depth-first search to compute the DFS order of the basic blocks. This
is used to compute the dominator tree. The sequence of basic blocks in
the DFS order is stored in `self.dfs_walk`. The DFS order of each basic
block is stored in `self.dfs_order`.
"""
visited.add(entry)

for bb in entry.cfg_out:
if bb not in visited:
self._compute_dfs(bb, visited)
@cached_property
def dfs_walk(self) -> list[IRBasicBlock]:
return list(self.cfg.dfs_walk)

self.dfs_walk.append(entry)
self.dfs_order[entry] = len(self.dfs_walk)
@cached_property
def dfs_order(self) -> dict[IRBasicBlock, int]:
return {bb: idx for idx, bb in enumerate(self.dfs_walk)}

def as_graph(self) -> str:
"""
Expand Down
15 changes: 8 additions & 7 deletions vyper/venom/analysis/liveness.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@ class LivenessAnalysis(IRAnalysis):
"""

def analyze(self):
self.analyses_cache.request_analysis(CFGAnalysis)
cfg = self.analyses_cache.request_analysis(CFGAnalysis)
self._reset_liveness()

self._worklist = deque()
self._worklist.extend(self.function.get_basic_blocks())
worklist = deque(reversed(list(cfg.topsort())))

while len(worklist) > 0:
bb = worklist.popleft()

while len(self._worklist) > 0:
changed = False
bb = self._worklist.popleft()
changed |= self._calculate_out_vars(bb)
changed |= self._calculate_liveness(bb)

# recompute liveness for basic blocks pointing into
# this basic block
if changed:
self._worklist.extend(bb.cfg_in)
worklist.extend(bb.cfg_in)

def _reset_liveness(self) -> None:
for bb in self.function.get_basic_blocks():
Expand Down Expand Up @@ -64,7 +65,7 @@ def _calculate_out_vars(self, bb: IRBasicBlock) -> bool:
bb.out_vars = OrderedSet()
for out_bb in bb.cfg_out:
target_vars = self.input_vars_from(bb, out_bb)
bb.out_vars = bb.out_vars.union(target_vars)
bb.out_vars.update(target_vars)
return out_vars != bb.out_vars

# calculate the input variables into self from source
Expand Down
37 changes: 26 additions & 11 deletions vyper/venom/basicblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
COMMUTATIVE_INSTRUCTIONS = frozenset(["add", "mul", "smul", "or", "xor", "and", "eq"])

if TYPE_CHECKING:
from vyper.venom.function import IRFunction

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'IRFunction' may not be defined if module
vyper.venom.function
is imported before module
vyper.venom.basicblock
, as the
definition
of IRFunction occurs after the cyclic
import
of vyper.venom.basicblock.


class IRDebugInfo:
Expand Down Expand Up @@ -185,15 +185,6 @@
self.value = value
self.is_symbol = is_symbol

def __eq__(self, other):
# no need for is_symbol to participate in equality
return super().__eq__(other)

def __hash__(self):
# __hash__ is required when __eq__ is overridden --
# https://docs.python.org/3/reference/datamodel.html#object.__hash__
return super().__hash__()


class IRInstruction:
"""
Expand Down Expand Up @@ -393,7 +384,6 @@
# stack items which this basic block produces
out_vars: OrderedSet[IRVariable]

reachable: OrderedSet["IRBasicBlock"]
is_reachable: bool = False

def __init__(self, label: IRLabel, parent: "IRFunction") -> None:
Expand All @@ -404,7 +394,6 @@
self.cfg_in = OrderedSet()
self.cfg_out = OrderedSet()
self.out_vars = OrderedSet()
self.reachable = OrderedSet()
self.is_reachable = False

def add_cfg_in(self, bb: "IRBasicBlock") -> None:
Expand Down Expand Up @@ -495,6 +484,32 @@
for instruction in self.instructions:
instruction.replace_operands(replacements)

def fix_phi_instructions(self):
cfg_in_labels = tuple(bb.label for bb in self.cfg_in)

needs_sort = False
for inst in self.instructions:
if inst.opcode != "phi":
continue

labels = inst.get_label_operands()
for label in labels:
if label not in cfg_in_labels:
needs_sort = True
inst.remove_phi_operand(label)

op_len = len(inst.operands)
if op_len == 2:
inst.opcode = "store"
inst.operands = [inst.operands[1]]
elif op_len == 0:
inst.opcode = "nop"
inst.output = None
inst.operands = []

if needs_sort:
self.instructions.sort(key=lambda inst: inst.opcode != "phi")

def get_assignments(self):
"""
Get all assignments in basic block.
Expand Down
60 changes: 16 additions & 44 deletions vyper/venom/function.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import Iterator, Optional

from vyper.codegen.ir_node import IRnode
from vyper.utils import OrderedSet
from vyper.venom.basicblock import CFG_ALTERING_INSTRUCTIONS, IRBasicBlock, IRLabel, IRVariable
from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRVariable

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'IRBasicBlock' may not be defined if module
vyper.venom.basicblock
is imported before module
vyper.venom.function
, as the
definition
of IRBasicBlock occurs after the cyclic
import
of vyper.venom.function.

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'IRLabel' may not be defined if module
vyper.venom.basicblock
is imported before module
vyper.venom.function
, as the
definition
of IRLabel occurs after the cyclic
import
of vyper.venom.function.

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'IRVariable' may not be defined if module
vyper.venom.basicblock
is imported before module
vyper.venom.function
, as the
definition
of IRVariable occurs after the cyclic
import
of vyper.venom.function.


class IRFunction:
Expand Down Expand Up @@ -89,60 +88,33 @@
return f"%{self.last_variable}"

def remove_unreachable_blocks(self) -> int:
self._compute_reachability()
# Remove unreachable basic blocks
# pre: requires CFG analysis!
# NOTE: should this be a pass?

removed = []
removed = set()

# Remove unreachable basic blocks
for bb in self.get_basic_blocks():
if not bb.is_reachable:
removed.append(bb)
removed.add(bb)

for bb in removed:
self.remove_basic_block(bb)

# Remove phi instructions that reference removed basic blocks
for bb in removed:
for out_bb in bb.cfg_out:
out_bb.remove_cfg_in(bb)
for inst in out_bb.instructions:
if inst.opcode != "phi":
continue
in_labels = inst.get_label_operands()
if bb.label in in_labels:
inst.remove_phi_operand(bb.label)
op_len = len(inst.operands)
if op_len == 2:
inst.opcode = "store"
inst.operands = [inst.operands[1]]
elif op_len == 0:
out_bb.remove_instruction(inst)

return len(removed)

def _compute_reachability(self) -> None:
"""
Compute reachability of basic blocks.
"""
for bb in self.get_basic_blocks():
bb.reachable = OrderedSet()
bb.is_reachable = False
modified = False
for in_bb in list(bb.cfg_in):
if in_bb not in removed:
continue

self._compute_reachability_from(self.entry)
modified = True
bb.remove_cfg_in(in_bb)

def _compute_reachability_from(self, bb: IRBasicBlock) -> None:
"""
Compute reachability of basic blocks from bb.
"""
if bb.is_reachable:
return
bb.is_reachable = True
for inst in bb.instructions:
if inst.opcode in CFG_ALTERING_INSTRUCTIONS:
for op in inst.get_label_operands():
out_bb = self.get_basic_block(op.value)
bb.reachable.add(out_bb)
self._compute_reachability_from(out_bb)
if modified or True:
bb.fix_phi_instructions()

return len(removed)

@property
def normalized(self) -> bool:
Expand Down
Loading
Loading