Skip to content

Commit

Permalink
add equivalence analysis
Browse files Browse the repository at this point in the history
use equivalence analysis to reduce swaps
  • Loading branch information
charles-cooper committed Sep 28, 2024
1 parent 1bf0173 commit 54d7e97
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 6 deletions.
37 changes: 37 additions & 0 deletions vyper/venom/analysis/equivalent_vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from vyper.utils import OrderedSet
from vyper.venom.analysis.analysis import IRAnalysis
from vyper.venom.basicblock import IRVariable
from vyper.venom.analysis.dfg import DFGAnalysis


class VarEquivalenceAnalysis(IRAnalysis):
"""
Generate equivalence sets of variables
"""
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]

if source in equivalence_set:
equivalence_set[var] = equivalence_set[source]
continue
else:
assert var not in equivalence_set
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]
51 changes: 45 additions & 6 deletions vyper/venom/venom_to_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from vyper.utils import MemoryPositions, OrderedSet
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.analysis.liveness import LivenessAnalysis
from vyper.venom.analysis.equivalent_vars import VarEquivalenceAnalysis
from vyper.venom.basicblock import (
IRBasicBlock,
IRInstruction,
Expand All @@ -25,6 +26,10 @@
from vyper.venom.passes.normalization import NormalizationPass
from vyper.venom.stack_model import StackModel

DEBUG_SHOW_COST = True
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,10 @@ 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):
stack.poke(final_stack_depth, op)
stack.poke(depth, to_swap)
continue

cost += self.swap(assembly, stack, depth)
Expand Down Expand Up @@ -276,6 +285,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 @@ -291,8 +306,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 @@ -415,6 +436,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 @@ -531,10 +559,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 @@ -556,7 +595,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

0 comments on commit 54d7e97

Please sign in to comment.