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]: common subexpression elimination pass #4241

Open
wants to merge 120 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
33ef4db
start cse
HodanPlodky Sep 4, 2024
52a3470
cse start
HodanPlodky Sep 4, 2024
c2d60ed
only one inst handling
HodanPlodky Sep 9, 2024
3087650
cleanup + fix
HodanPlodky Sep 10, 2024
bad3f69
effects start
HodanPlodky Sep 12, 2024
9798a39
correct compare for joins
HodanPlodky Sep 13, 2024
ea92aa0
fix for calls and instruction without output (i.e. stores)
HodanPlodky Sep 13, 2024
c754574
small cleanup
HodanPlodky Sep 16, 2024
e6a8551
small cleanup
HodanPlodky Sep 16, 2024
6136f83
basic test created and fix small fix
HodanPlodky Sep 16, 2024
2d7e1a4
clean up of the debug
HodanPlodky Sep 16, 2024
4b62958
Update vyper/venom/analysis/available_expression.py - better hash method
HodanPlodky Sep 16, 2024
fbf2964
Update vyper/venom/analysis/available_expression.py - Incorrect gramm…
HodanPlodky Sep 16, 2024
86977f5
fix for some error in cancun version of experimental codegen test
HodanPlodky Sep 17, 2024
49e6949
fix for log opcode and lint
HodanPlodky Sep 17, 2024
de3c602
handling the different size of the expressions
HodanPlodky Sep 18, 2024
ae98b46
Merge branch 'vyperlang:master' into feat/cse
HodanPlodky Sep 22, 2024
b286eee
fixes in handling effects + bigger expression
HodanPlodky Sep 23, 2024
14b8963
fixes in handling effects
HodanPlodky Sep 24, 2024
e957b04
fixes (to better version) and style changes
HodanPlodky Sep 25, 2024
63f27fe
perf fixes + better order
HodanPlodky Sep 26, 2024
9ce9dd8
quick fix just for dft but since it is being rework it is just hot fix
HodanPlodky Sep 26, 2024
a3585ed
Merge branch 'master' into feat/cse
HodanPlodky Sep 30, 2024
775f0a9
created possibility to change size of the expression at pass level
HodanPlodky Sep 30, 2024
ea944e6
created possibility to change size of the expression at pass level (l…
HodanPlodky Sep 30, 2024
ca23cde
Merge branch 'master' into feat/cse
HodanPlodky Oct 5, 2024
018b6a9
fixes after merge
HodanPlodky Oct 5, 2024
94a9a6e
better results this order
HodanPlodky Oct 6, 2024
7d5b6aa
used new effects data structure to reduce duplication of code
HodanPlodky Oct 6, 2024
2de7731
add test for commutative instructions
harkal Oct 10, 2024
9efaea1
simplification of `same()` and replacement by `__eq__`
harkal Oct 10, 2024
22e0627
Merge branch 'master' into feat/cse
harkal Oct 10, 2024
4339697
different branches test
HodanPlodky Oct 10, 2024
8f76730
cleanup tests
harkal Oct 10, 2024
30ed887
add `same()` to `IROperand`
harkal Oct 10, 2024
2d33dc7
fix equality of expressions
harkal Oct 10, 2024
866ee62
Merge branch 'master' into feat/cse
harkal Oct 10, 2024
145ee31
comments and fix of the hash function so it should hold that if x and…
HodanPlodky Oct 10, 2024
f3c66d0
Merge branch 'master' into feat/cse
harkal Oct 10, 2024
6f9800f
bit more comments
HodanPlodky Oct 10, 2024
c6a825a
lint
HodanPlodky Oct 10, 2024
674577a
comments and removed some unnecessery code
HodanPlodky Oct 11, 2024
479271e
lint
HodanPlodky Oct 11, 2024
47e0a0e
added test for sanity check on effects and removed the change in the …
HodanPlodky Oct 14, 2024
4c9288e
Merge branch 'master' into feat/cse
HodanPlodky Oct 14, 2024
6e70e6e
fix after merge
HodanPlodky Oct 14, 2024
f244a13
fix after merge
HodanPlodky Oct 14, 2024
f3b709b
fixed circular import from commutative instruction
HodanPlodky Oct 14, 2024
b9caf57
idempotent instruction better support and test for the logs
HodanPlodky Oct 15, 2024
ee7a293
test cleanup and lint
HodanPlodky Oct 15, 2024
b462bdc
Merge branch 'master' into feat/cse
HodanPlodky Oct 15, 2024
7ca27b6
used commutative
HodanPlodky Oct 15, 2024
74aab92
caching of read and writes effects
HodanPlodky Oct 16, 2024
994ab14
Merge branch 'master' into feat/cse
HodanPlodky Oct 17, 2024
00b5e9e
better same compare and caching of exprs and depth
HodanPlodky Oct 18, 2024
1b576f3
more caching
HodanPlodky Oct 18, 2024
c99341f
should be better reevaluating
HodanPlodky Oct 18, 2024
3b3a546
idempotent rename and small clean
HodanPlodky Oct 18, 2024
3e45b31
replaced lattice structure by just dicts to make it more explicit
HodanPlodky Oct 18, 2024
73bd8ee
lint
HodanPlodky Oct 18, 2024
b6e0048
added small heuristic for small expressions
HodanPlodky Oct 21, 2024
44fb876
Merge branch 'master' into feat/cse
HodanPlodky Oct 28, 2024
45c3612
add some review
charles-cooper Nov 10, 2024
5e7f603
basic refactors and bit of improvement
HodanPlodky Nov 11, 2024
2434789
Merge branch 'master' into feat/cse
HodanPlodky Nov 11, 2024
709dc4d
Merge branch 'master' into feat/cse
HodanPlodky Nov 12, 2024
33f3dae
weird patch
HodanPlodky Nov 13, 2024
4e36a9c
different way of same fuction implementation
HodanPlodky Nov 18, 2024
dc07dff
small cleanup and more unintresting instruction
HodanPlodky Nov 18, 2024
4d57171
removed depth
HodanPlodky Nov 18, 2024
1a9c9ab
lint
HodanPlodky Nov 18, 2024
639d70b
removed forgoten comment
HodanPlodky Nov 18, 2024
bd89aae
Merge branch 'master' into feat/cse
HodanPlodky Nov 22, 2024
5591c53
Merge branch 'master' into feat/cse
HodanPlodky Nov 28, 2024
9b1a92d
Merge branch 'master' into feat/cse
HodanPlodky Dec 12, 2024
d4dcc8e
Merge branch 'master' into feat/cse
HodanPlodky Jan 5, 2025
06c627f
effect IMMUTABLE can interfere with MEMORY
HodanPlodky Jan 6, 2025
0971f58
early return
HodanPlodky Jan 6, 2025
eb136b8
Merge branch 'master' into feat/cse
HodanPlodky Jan 12, 2025
6cd1927
better handling of available expressions
HodanPlodky Jan 20, 2025
08a0bd4
lint
HodanPlodky Jan 20, 2025
752b70f
Merge branch 'master' into feat/cse
HodanPlodky Jan 20, 2025
938c144
lint
HodanPlodky Jan 20, 2025
ab04c84
removed unused
HodanPlodky Jan 21, 2025
58393ec
Merge branch 'master' into feat/cse
HodanPlodky Jan 21, 2025
8a35af2
inter bb
HodanPlodky Jan 24, 2025
e66b348
order fix
HodanPlodky Jan 24, 2025
1d212ac
fixes for inter bb
HodanPlodky Jan 27, 2025
d3e2d64
removed unused code
HodanPlodky Jan 27, 2025
9ca6609
Merge branch 'master' into feat/cse
HodanPlodky Jan 28, 2025
5e406d5
small change in removing instructions
HodanPlodky Jan 28, 2025
09afa3e
merge
HodanPlodky Jan 28, 2025
4755b12
test rewrite with new parser + small fix
HodanPlodky Jan 28, 2025
042f584
non indempotent instruction test
HodanPlodky Jan 29, 2025
d35bed0
loops test and fixes
HodanPlodky Jan 29, 2025
daaa20c
loops without substitution test + simple inst test
HodanPlodky Jan 29, 2025
32384f1
comments
HodanPlodky Jan 29, 2025
5ad8951
removed unreachable
HodanPlodky Jan 30, 2025
bbc4c86
small refactors
HodanPlodky Jan 30, 2025
4573c95
small refactors
HodanPlodky Jan 30, 2025
4727ba5
bb input cache
HodanPlodky Jan 30, 2025
7a59827
removed unnecessary copies
HodanPlodky Jan 30, 2025
5fb3aa3
early return in get_expression
HodanPlodky Jan 30, 2025
84cf322
lint
HodanPlodky Jan 31, 2025
1e976e7
comments in tests
HodanPlodky Jan 31, 2025
34ba28d
more test for effects
HodanPlodky Jan 31, 2025
43a059b
Merge branch 'master' into feat/cse
HodanPlodky Jan 31, 2025
0d01653
comments and removed get_operands method
HodanPlodky Jan 31, 2025
2cf6090
test renames
HodanPlodky Feb 1, 2025
1cc93db
removed _check_pre_post_fn and _check_no_change_fn
HodanPlodky Feb 1, 2025
42e15a4
added frozen on `_Expression` class
HodanPlodky Feb 3, 2025
90e03f0
Removed unnecessary `RemovedUnusedVariablesPass`
HodanPlodky Feb 3, 2025
6a79288
remove unnecessary effect
HodanPlodky Feb 3, 2025
3621c05
added `RemovedUnusedVariablePass` back + deep effects
HodanPlodky Feb 6, 2025
30eee09
Merge branch 'master' into feat/cse
HodanPlodky Feb 6, 2025
62cf3cb
small fixes
HodanPlodky Feb 6, 2025
3fd3a0a
comment
HodanPlodky Feb 7, 2025
d5ede4f
lint
HodanPlodky Feb 7, 2025
2085b80
review comments
charles-cooper Feb 6, 2025
8785d61
review
charles-cooper Feb 7, 2025
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
23 changes: 23 additions & 0 deletions tests/unit/compiler/venom/test_common_subexpression_elimination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from vyper.venom.context import IRContext
from vyper.venom.passes.common_subexpression_elimination import CSE
from vyper.venom.passes.extract_literals import ExtractLiteralsPass
from vyper.venom.analysis.analysis import IRAnalysesCache

def test_common_subexpression_elimination():
ctx = IRContext()
fn = ctx.create_function("test")
bb = fn.get_basic_block()
op = bb.append_instruction("store", 10)
sum_1 = bb.append_instruction("add", op, 10)
bb.append_instruction("mul", sum_1, 10)
sum_2 = bb.append_instruction("add", op, 10)
bb.append_instruction("mul", sum_2, 10)
bb.append_instruction("stop")


ac = IRAnalysesCache(fn)
CSE(ac, fn).run_pass()
ExtractLiteralsPass(ac, fn).run_pass()

assert sum(1 for inst in bb.instructions if inst.opcode == "add") == 1, "wrong number of adds"
assert sum(1 for inst in bb.instructions if inst.opcode == "mul") == 1, "wrong number of muls"
2 changes: 2 additions & 0 deletions vyper/venom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from vyper.venom.passes.simplify_cfg import SimplifyCFGPass
from vyper.venom.passes.store_elimination import StoreElimination
from vyper.venom.venom_to_assembly import VenomCompiler
from vyper.venom.passes.common_subexpression_elimination import CSE

DEFAULT_OPT_LEVEL = OptimizationLevel.default()

Expand Down Expand Up @@ -54,6 +55,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None:
SimplifyCFGPass(ac, fn).run_pass()
AlgebraicOptimizationPass(ac, fn).run_pass()
BranchOptimizationPass(ac, fn).run_pass()
CSE(ac, fn).run_pass()
ExtractLiteralsPass(ac, fn).run_pass()
RemoveUnusedVariablesPass(ac, fn).run_pass()
DFTPass(ac, fn).run_pass()
Expand Down
196 changes: 196 additions & 0 deletions vyper/venom/analysis/available_expression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
from vyper.venom.analysis.analysis import IRAnalysis
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.analysis.dfg import DFGAnalysis
from vyper.venom.analysis.cfg import CFGAnalysis
from vyper.venom.context import IRFunction
from vyper.venom.basicblock import IRBasicBlock
from vyper.venom.basicblock import IRInstruction
from vyper.venom.basicblock import IRLiteral
Fixed Show fixed Hide fixed
from vyper.venom.basicblock import IRVariable
Fixed Show fixed Hide fixed
from vyper.venom.basicblock import IROperand
from vyper.venom.basicblock import BB_TERMINATORS
from vyper.utils import OrderedSet
from dataclasses import dataclass
from collections import deque

@dataclass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set frozen=True? this will let use do things like cache properties more safely.

class _Expression:
first_inst : IRInstruction
opcode: str
operands : list[IROperand]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this can be IROperand | _Expression


def __eq__(self, other):
if not isinstance(other, _Expression):
return False
return self.first_inst == other.first_inst

def __hash__(self) -> int:
res : int = hash(self.opcode)
for op in self.operands:
res ^= hash(op)
return res
HodanPlodky marked this conversation as resolved.
Show resolved Hide resolved

def __repr__(self) -> str:
if self.opcode == "store":
assert len(self.operands) == 1, "wrong store"
Fixed Show fixed Hide fixed
return repr(self.operands[0])
res = self.opcode + " [ "
for op in self.operands:
res += repr(op) + " "
res += "]"
return res

def contains_expr(self, expr : "_Expression") -> bool:
for op in self.operands:
if op == expr:
return True
if isinstance(op, _Expression) and op.contains_expr(expr):
return True
return False

class _BBLattice:
data : dict[IRInstruction, OrderedSet[_Expression]]
out : OrderedSet[_Expression]
in_cache: OrderedSet[_Expression] | None

def __init__(self, bb : IRBasicBlock):
self.data = dict()
self.out = OrderedSet()
self.in_cache = None
for inst in bb.instructions:
self.data[inst] = OrderedSet()

_UNINTRESTING_OPCODES = [
HodanPlodky marked this conversation as resolved.
Show resolved Hide resolved
"store",
"param",
"offset",
"phi",
"nop",
]

_ALL = ("storage", "transient", "memory", "immutables", "balance", "returndata")

writes = {
"sstore": ("storage"),
"tstore": ("transient"),
"mstore": ("memory"),
"istore": ("immutables"),
"call": _ALL,
"delegatecall": _ALL,
"staticcall": ("memory"),
"create": _ALL,
"create2": _ALL,
"invoke": _ALL, # could be smarter, look up the effects of the invoked function
"dloadbytes": ("memory"),
"returndatacopy": ("memory"),
"calldatacopy": ("memory"),
"codecopy": ("memory"),
"extcodecopy": ("memory"),
"mcopy": ("memory"),
}
reads = {
"sload": ("storage"),
"tload": ("transient"),
"iload": ("immutables"),
"mload": ("memory"),
"mcopy": ("memory"),
"call": _ALL,
"delegatecall": _ALL,
"staticcall": _ALL,
"returndatasize": ("returndata"),
"returndatacopy": ("returndata"),
"balance": ("balance"),
"selfbalance": ("balance"),
"log": ("memory"),
"revert": ("memory"),
"return": ("memory"),
"sha3": ("memory"),
}


class _FunctionLattice:
data : dict[IRBasicBlock, _BBLattice]

def __init__(self, function : IRFunction):
self.data = dict()
for bb in function.get_basic_blocks():
self.data[bb] = _BBLattice(bb)

class AvailableExpressionAnalysis(IRAnalysis):
expressions : OrderedSet[_Expression] = OrderedSet()
inst_to_expr: dict[IRInstruction, _Expression] = dict()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memory leak

dfg: DFGAnalysis
lattice : _FunctionLattice

def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction):
super().__init__(analyses_cache, function)
self.analyses_cache.request_analysis(CFGAnalysis)
dfg = self.analyses_cache.request_analysis(DFGAnalysis)
assert isinstance(dfg, DFGAnalysis)
self.dfg = dfg

self.lattice = _FunctionLattice(function)

def analyze(self, *args, **kwargs):
worklist = deque()
worklist.append(self.function.entry)
while len(worklist) > 0:
bb : IRBasicBlock = worklist.popleft()
changed = self._handle_bb(bb)

if changed:
for out in bb.cfg_out:
if out not in worklist:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we require uniqueness in the worklist then we should use OrderedSet. it has a pop() function which is O(1) LIFO pop.

worklist.append(out)

def _handle_bb(self, bb : IRBasicBlock) -> bool:
available_expr : OrderedSet[_Expression] = OrderedSet()
if len(bb.cfg_in) > 0:
available_expr = OrderedSet.intersection(*(self.lattice.data[in_bb].out for in_bb in bb.cfg_in))

bb_lat = self.lattice.data[bb]
if bb_lat.in_cache is not None and available_expr == bb_lat.in_cache:
return False
bb_lat.in_cache = available_expr
change = False
for inst in bb.instructions:
if (inst.opcode in _UNINTRESTING_OPCODES
or inst.opcode in BB_TERMINATORS):
continue
if available_expr != bb_lat.data[inst]:
bb_lat.data[inst] = available_expr.copy()
change |= True


inst_expr = self.get_expression(inst, available_expr)
write_effects = writes.get(inst_expr.opcode, ())
for expr in available_expr.copy():
if expr.contains_expr(inst_expr):
available_expr.remove(expr)
read_effects = reads.get(expr.opcode, ())
if any(eff in write_effects for eff in read_effects):
available_expr.remove(expr)

if "call" not in inst.opcode and inst.opcode != "invoke":
available_expr.add(inst_expr)

if available_expr != bb_lat.out:
bb_lat.out = available_expr.copy()
change |= True

return change

def get_expression(self, inst: IRInstruction, available_exprs : OrderedSet[_Expression] | None = None) -> _Expression:
if available_exprs is None:
available_exprs = self.lattice.data[inst.parent].data[inst]
operands: list[IROperand] = inst.operands.copy()
expr = _Expression(inst, inst.opcode, operands)
for e in available_exprs:
if e.opcode == expr.opcode and e.operands == expr.operands:
return e

return expr

def get_available(self, inst : IRInstruction) -> OrderedSet[_Expression]:
return self.lattice.data[inst.parent].data[inst]

65 changes: 65 additions & 0 deletions vyper/venom/passes/common_subexpression_elimination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from vyper.venom.passes.base_pass import IRPass
from vyper.venom.analysis.available_expression import AvailableExpressionAnalysis
from vyper.venom.analysis.liveness import LivenessAnalysis
from vyper.venom.analysis.dfg import DFGAnalysis
from vyper.venom.basicblock import IRInstruction
from vyper.venom.basicblock import IRVariable
from vyper.venom.basicblock import IRBasicBlock
from vyper.utils import OrderedSet

class CSE(IRPass):
available_expression_analysis : AvailableExpressionAnalysis

def run_pass(self, *args, **kwargs):
available_expression_analysis = self.analyses_cache.request_analysis(AvailableExpressionAnalysis)
assert isinstance(available_expression_analysis, AvailableExpressionAnalysis)
self.available_expression_analysis = available_expression_analysis

while True:
replace_dict = self._find_replaceble()
if len(replace_dict) == 0:
return
self._replace(replace_dict)
self.analyses_cache.invalidate_analysis(DFGAnalysis)
self.analyses_cache.invalidate_analysis(LivenessAnalysis)

# return instruction and to which instruction it could
# replaced by
def _find_replaceble(self) -> dict[IRInstruction, IRInstruction]:
res : dict[IRInstruction, IRInstruction] = dict()
for bb in self.function.get_basic_blocks():
for inst in bb.instructions:
inst_expr = self.available_expression_analysis.get_expression(inst)
avail = self.available_expression_analysis.get_available(inst)
if inst_expr in avail:
res[inst] = inst_expr.first_inst

return res

def _replace(self, replace_dict : dict[IRInstruction, IRInstruction]):
for (orig, to) in replace_dict.items():
while to in replace_dict.keys():
to = replace_dict[to]
self._replace_inst(orig, to)

def _replace_inst(self, orig_inst : IRInstruction, to_inst : IRInstruction):
visited: OrderedSet[IRBasicBlock] = OrderedSet()
if orig_inst.output is not None:
assert isinstance(orig_inst.output, IRVariable), f"not var {orig_inst}"
assert isinstance(to_inst.output, IRVariable), f"not var {to_inst}"
self._replace_inst_r(orig_inst.parent, orig_inst.output, to_inst.output, visited)
orig_inst.parent.remove_instruction(orig_inst)

def _replace_inst_r(self, bb : IRBasicBlock, orig : IRVariable, to : IRVariable, visited : OrderedSet[IRBasicBlock]):
if bb in visited:
return
visited.add(bb)

for inst in bb.instructions:
for i in range(len(inst.operands)):
op = inst.operands[i]
if op == orig:
inst.operands[i] = to

for out in bb.cfg_out:
self._replace_inst_r(out, orig, to, visited)
Loading