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]: store expansion pass #4068

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6cf7b6b
feat[venom]: extract literals pass
charles-cooper May 30, 2024
ef7c369
feat: store expansion pass
charles-cooper May 31, 2024
ff700b4
lint
charles-cooper May 31, 2024
ea9b1c5
remove inter-bb restriction
charles-cooper May 31, 2024
adbf01c
don't replace first use
charles-cooper May 31, 2024
aa2234c
fix bugs
charles-cooper Jun 1, 2024
b6b7aed
allow inter-bb
charles-cooper Jun 1, 2024
a71cad8
lint
charles-cooper Jun 1, 2024
163979b
fix a bug
charles-cooper Jun 4, 2024
61ea577
fix algorithm a bit
charles-cooper Jun 5, 2024
e3e926d
Merge branch 'master' into feat/store-expansion
charles-cooper Sep 9, 2024
ab4055e
Merge branch 'master' into feat/store-expansion
charles-cooper Sep 19, 2024
d233a75
Merge branch 'master' into feat/store-expansion
charles-cooper Sep 25, 2024
669c170
fix store expansion - expand close to the use site, not the productio…
charles-cooper Sep 25, 2024
248370d
fix lint
charles-cooper Sep 25, 2024
7313acf
fuse store expansion and literal extraction passes
charles-cooper Sep 25, 2024
fe215d9
cleanup - rename extract literals to store_expansion
charles-cooper Sep 25, 2024
a22f5c9
reorder store expansion and unused var elimination
charles-cooper Sep 25, 2024
0b0851a
Merge branch 'master' into feat/store-expansion
charles-cooper Sep 28, 2024
f843eca
simplify emit_input_operands
charles-cooper Sep 28, 2024
1bf0173
remove a heuristic
charles-cooper Sep 28, 2024
54d7e97
add equivalence analysis
charles-cooper Sep 21, 2024
42785ed
fix lint
charles-cooper Sep 28, 2024
a367b1d
fix lint, tests
charles-cooper Sep 28, 2024
b8ae0d1
add a note
charles-cooper Sep 29, 2024
df469c6
strength an assertion
charles-cooper Sep 29, 2024
2ae3ea9
add some comments
charles-cooper Sep 29, 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
12 changes: 9 additions & 3 deletions tests/unit/compiler/venom/test_duplicate_operands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from vyper.compiler.settings import OptimizationLevel
from vyper.venom import generate_assembly_experimental
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.context import IRContext
from vyper.venom.passes.store_expansion import StoreExpansionPass


def test_duplicate_operands():
Expand All @@ -13,7 +15,7 @@ def test_duplicate_operands():
%3 = mul %1, %2
stop

Should compile to: [PUSH1, 10, DUP1, DUP1, DUP1, ADD, MUL, POP, STOP]
Should compile to: [PUSH1, 10, DUP1, DUP2, ADD, MUL, POP, STOP]
"""
ctx = IRContext()
fn = ctx.create_function("test")
Expand All @@ -23,5 +25,9 @@ def test_duplicate_operands():
bb.append_instruction("mul", sum_, op)
bb.append_instruction("stop")

asm = generate_assembly_experimental(ctx, optimize=OptimizationLevel.GAS)
assert asm == ["PUSH1", 10, "DUP1", "DUP1", "ADD", "MUL", "POP", "STOP"]
ac = IRAnalysesCache(fn)
StoreExpansionPass(ac, fn).run_pass()

optimize = OptimizationLevel.GAS
asm = generate_assembly_experimental(ctx, optimize=optimize)
assert asm == ["PUSH1", 10, "DUP1", "DUP2", "ADD", "MUL", "POP", "STOP"]
3 changes: 2 additions & 1 deletion tests/unit/compiler/venom/test_stack_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ def test_cleanup_stack():
bb = fn.get_basic_block()
ret_val = bb.append_instruction("param")
op = bb.append_instruction("store", 10)
bb.append_instruction("add", op, op)
op2 = bb.append_instruction("store", op)
bb.append_instruction("add", op, op2)
bb.append_instruction("ret", ret_val)

asm = generate_assembly_experimental(ctx, optimize=OptimizationLevel.GAS)
Expand Down
5 changes: 3 additions & 2 deletions vyper/venom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass
from vyper.venom.passes.branch_optimization import BranchOptimizationPass
from vyper.venom.passes.dft import DFTPass
from vyper.venom.passes.extract_literals import ExtractLiteralsPass
from vyper.venom.passes.make_ssa import MakeSSA
from vyper.venom.passes.mem2var import Mem2Var
from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass
from vyper.venom.passes.sccp import SCCP
from vyper.venom.passes.simplify_cfg import SimplifyCFGPass
from vyper.venom.passes.store_elimination import StoreElimination
from vyper.venom.passes.store_expansion import StoreExpansionPass
from vyper.venom.venom_to_assembly import VenomCompiler

DEFAULT_OPT_LEVEL = OptimizationLevel.default()
Expand Down Expand Up @@ -54,8 +54,9 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None:
SimplifyCFGPass(ac, fn).run_pass()
AlgebraicOptimizationPass(ac, fn).run_pass()
BranchOptimizationPass(ac, fn).run_pass()
ExtractLiteralsPass(ac, fn).run_pass()
RemoveUnusedVariablesPass(ac, fn).run_pass()

StoreExpansionPass(ac, fn).run_pass()
DFTPass(ac, fn).run_pass()


Expand Down
41 changes: 41 additions & 0 deletions vyper/venom/analysis/equivalent_vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from vyper.venom.analysis.analysis import IRAnalysis
from vyper.venom.analysis.dfg import DFGAnalysis
from vyper.venom.basicblock import IRVariable


class VarEquivalenceAnalysis(IRAnalysis):
"""
Generate equivalence sets of variables. This is used to avoid swapping
variables which are the same during venom_to_assembly. Theoretically,
the DFTPass should order variable declarations optimally, but, it is
not aware of the "pickaxe" heuristic in venom_to_assembly, so they can
interfere.
"""

def analyze(self):
dfg = self.analyses_cache.request_analysis(DFGAnalysis)

equivalence_set: dict[IRVariable, int] = {}

for bag, (var, inst) in enumerate(dfg._dfg_outputs.items()):
if inst.opcode != "store":
continue

source = inst.operands[0]

assert var not in equivalence_set # invariant
if source in equivalence_set:
equivalence_set[var] = equivalence_set[source]
continue
else:
equivalence_set[var] = bag
equivalence_set[source] = bag

self._equivalence_set = equivalence_set

def equivalent(self, var1, var2):
if var1 not in self._equivalence_set:
return False
if var2 not in self._equivalence_set:
return False
return self._equivalence_set[var1] == self._equivalence_set[var2]
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from vyper.venom.analysis.dfg import DFGAnalysis
from vyper.venom.analysis.liveness import LivenessAnalysis
from vyper.venom.basicblock import IRInstruction, IRLiteral
from vyper.venom.basicblock import IRInstruction, IRLiteral, IRVariable
from vyper.venom.passes.base_pass import IRPass


class ExtractLiteralsPass(IRPass):
class StoreExpansionPass(IRPass):
"""
This pass extracts literals so that they can be reordered by the DFT pass
This pass extracts literals and variables so that they can be
reordered by the DFT pass
"""

def run_pass(self):
Expand All @@ -20,7 +21,7 @@ def _process_bb(self, bb):
i = 0
while i < len(bb.instructions):
inst = bb.instructions[i]
if inst.opcode in ("store", "offset"):
if inst.opcode in ("store", "offset", "phi", "param"):
i += 1
continue

Expand All @@ -29,9 +30,11 @@ def _process_bb(self, bb):
if inst.opcode == "log" and j == 0:
continue

if isinstance(op, IRLiteral):
if isinstance(op, (IRVariable, IRLiteral)):
var = self.function.get_next_variable()
to_insert = IRInstruction("store", [op], var)
bb.insert_instruction(to_insert, index=i)
inst.operands[j] = var
i += 1

i += 1
77 changes: 56 additions & 21 deletions vyper/venom/venom_to_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from vyper.utils import MemoryPositions, OrderedSet
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.analysis.equivalent_vars import VarEquivalenceAnalysis
from vyper.venom.analysis.liveness import LivenessAnalysis
from vyper.venom.basicblock import (
IRBasicBlock,
Expand All @@ -25,6 +26,10 @@
from vyper.venom.passes.normalization import NormalizationPass
from vyper.venom.stack_model import StackModel

DEBUG_SHOW_COST = False
if DEBUG_SHOW_COST:
import sys

# instructions which map one-to-one from venom to EVM
_ONE_TO_ONE_INSTRUCTIONS = frozenset(
[
Expand Down Expand Up @@ -152,6 +157,7 @@ def generate_evm(self, no_optimize: bool = False) -> list[str]:

NormalizationPass(ac, fn).run_pass()
self.liveness_analysis = ac.request_analysis(LivenessAnalysis)
self.equivalence = ac.request_analysis(VarEquivalenceAnalysis)

assert fn.normalized, "Non-normalized CFG!"

Expand Down Expand Up @@ -220,7 +226,11 @@ def _stack_reorder(
if depth == final_stack_depth:
continue

if op == stack.peek(final_stack_depth):
to_swap = stack.peek(final_stack_depth)
if self.equivalence.equivalent(op, to_swap):
# perform a "virtual" swap
stack.poke(final_stack_depth, op)
stack.poke(depth, to_swap)
continue

cost += self.swap(assembly, stack, depth)
Expand All @@ -240,19 +250,14 @@ def _emit_input_operands(
# been scheduled to be killed. now it's just a matter of emitting
# SWAPs, DUPs and PUSHes until we match the `ops` argument

# dumb heuristic: if the top of stack is not wanted here, swap
# it with something that is wanted
if ops and stack.height > 0 and stack.peek(0) not in ops:
for op in ops:
if isinstance(op, IRVariable) and op not in next_liveness:
self.swap_op(assembly, stack, op)
break
# to validate store expansion invariant -
# each op is emitted at most once.
seen: set[IROperand] = set()

emitted_ops = OrderedSet[IROperand]()
for op in ops:
if isinstance(op, IRLabel):
# invoke emits the actual instruction itself so we don't need to emit it here
# but we need to add it to the stack map
# invoke emits the actual instruction itself so we don't need
# to emit it here but we need to add it to the stack map
if inst.opcode != "invoke":
assembly.append(f"_sym_{op.value}")
stack.push(op)
Expand All @@ -267,13 +272,12 @@ def _emit_input_operands(
stack.push(op)
continue

if op in next_liveness and op not in emitted_ops:
self.dup_op(assembly, stack, op)

if op in emitted_ops:
if op in next_liveness:
self.dup_op(assembly, stack, op)

emitted_ops.add(op)
# guaranteed by store expansion
assert op not in seen, (op, seen)
seen.add(op)

def _generate_evm_for_basicblock_r(
self, asm: list, basicblock: IRBasicBlock, stack: StackModel
Expand All @@ -282,6 +286,12 @@ def _generate_evm_for_basicblock_r(
return
self.visited_basicblocks.add(basicblock)

if DEBUG_SHOW_COST:
print(basicblock, file=sys.stderr)

ref = asm
asm = []

# assembly entry point into the block
asm.append(f"_sym_{basicblock.label}")
asm.append("JUMPDEST")
Expand All @@ -297,8 +307,14 @@ def _generate_evm_for_basicblock_r(

asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness))

if DEBUG_SHOW_COST:
print(" ".join(map(str, asm)), file=sys.stderr)
print("\n", file=sys.stderr)

ref.extend(asm)

for bb in basicblock.reachable:
self._generate_evm_for_basicblock_r(asm, bb, stack.copy())
self._generate_evm_for_basicblock_r(ref, bb, stack.copy())

# pop values from stack at entry to bb
# note this produces the same result(!) no matter which basic block
Expand Down Expand Up @@ -380,6 +396,7 @@ def _generate_evm_for_instruction(
# find an instance of %13 *or* %14 in the stack and replace it with %56.
to_be_replaced = stack.peek(depth)
if to_be_replaced in next_liveness:
# this branch seems unreachable (maybe due to make_ssa)
# %13/%14 is still live(!), so we make a copy of it
self.dup(assembly, stack, depth)
stack.poke(0, ret)
Expand Down Expand Up @@ -421,6 +438,13 @@ def _generate_evm_for_instruction(
if cost_with_swap > cost_no_swap:
operands[-1], operands[-2] = operands[-2], operands[-1]

cost = self._stack_reorder([], stack, operands, dry_run=True)
if DEBUG_SHOW_COST and cost:
print("ENTER", inst, file=sys.stderr)
print(" HAVE", stack, file=sys.stderr)
print(" WANT", operands, file=sys.stderr)
print(" COST", cost, file=sys.stderr)

# final step to get the inputs to this instruction ordered
# correctly on the stack
self._stack_reorder(assembly, stack, operands)
Expand Down Expand Up @@ -537,10 +561,21 @@ def _generate_evm_for_instruction(
if inst.output not in next_liveness:
self.pop(assembly, stack)
else:
# peek at next_liveness to find the next scheduled item,
# and optimistically swap with it
# heuristic: peek at next_liveness to find the next scheduled
# item, and optimistically swap with it
if DEBUG_SHOW_COST:
stack0 = stack.copy()

next_scheduled = next_liveness.last()
self.swap_op(assembly, stack, next_scheduled)
cost = 0
if not self.equivalence.equivalent(inst.output, next_scheduled):
cost = self.swap_op(assembly, stack, next_scheduled)

if DEBUG_SHOW_COST and cost != 0:
print("ENTER", inst, file=sys.stderr)
print(" HAVE", stack0, file=sys.stderr)
print(" NEXT LIVENESS", next_liveness, file=sys.stderr)
print(" NEW_STACK", stack, file=sys.stderr)

return apply_line_numbers(inst, assembly)

Expand All @@ -562,7 +597,7 @@ def dup(self, assembly, stack, depth):
assembly.append(_evm_dup_for(depth))

def swap_op(self, assembly, stack, op):
self.swap(assembly, stack, stack.get_depth(op))
return self.swap(assembly, stack, stack.get_depth(op))

def dup_op(self, assembly, stack, op):
self.dup(assembly, stack, stack.get_depth(op))
Expand Down
Loading