Skip to content

Commit

Permalink
Merge branch 'master' into fix/dynarray-append-setter
Browse files Browse the repository at this point in the history
  • Loading branch information
charles-cooper authored May 30, 2024
2 parents b098c07 + dcec257 commit 2b93d79
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 3 deletions.
129 changes: 129 additions & 0 deletions tests/unit/compiler/venom/test_algebraic_optimizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import pytest

from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.basicblock import IRBasicBlock, IRLabel
from vyper.venom.context import IRContext
from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass
from vyper.venom.passes.make_ssa import MakeSSA
from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass


@pytest.mark.parametrize("iszero_count", range(5))
def test_simple_jump_case(iszero_count):
ctx = IRContext()
fn = ctx.create_function("_global")

bb = fn.get_basic_block()

br1 = IRBasicBlock(IRLabel("then"), fn)
fn.append_basic_block(br1)
br2 = IRBasicBlock(IRLabel("else"), fn)
fn.append_basic_block(br2)

p1 = bb.append_instruction("param")
op1 = bb.append_instruction("store", p1)
op2 = bb.append_instruction("store", 64)
op3 = bb.append_instruction("add", op1, op2)
jnz_input = op3

for _ in range(iszero_count):
jnz_input = bb.append_instruction("iszero", jnz_input)

bb.append_instruction("jnz", jnz_input, br1.label, br2.label)

br1.append_instruction("add", op3, 10)
br1.append_instruction("stop")
br2.append_instruction("add", op3, p1)
br2.append_instruction("stop")

ac = IRAnalysesCache(fn)
MakeSSA(ac, fn).run_pass()
AlgebraicOptimizationPass(ac, fn).run_pass()
RemoveUnusedVariablesPass(ac, fn).run_pass()

iszeros = [inst for inst in bb.instructions if inst.opcode == "iszero"]
removed_iszeros = iszero_count - len(iszeros)

assert removed_iszeros % 2 == 0
assert len(iszeros) == iszero_count % 2


@pytest.mark.parametrize("iszero_count", range(1, 5))
def test_simple_bool_cast_case(iszero_count):
ctx = IRContext()
fn = ctx.create_function("_global")

bb = fn.get_basic_block()

br1 = IRBasicBlock(IRLabel("then"), fn)
fn.append_basic_block(br1)

p1 = bb.append_instruction("param")
op1 = bb.append_instruction("store", p1)
op2 = bb.append_instruction("store", 64)
op3 = bb.append_instruction("add", op1, op2)
jnz_input = op3

for _ in range(iszero_count):
jnz_input = bb.append_instruction("iszero", jnz_input)

bb.append_instruction("mstore", jnz_input, p1)
bb.append_instruction("jmp", br1.label)

br1.append_instruction("add", op3, 10)
br1.append_instruction("stop")

ac = IRAnalysesCache(fn)
MakeSSA(ac, fn).run_pass()
AlgebraicOptimizationPass(ac, fn).run_pass()
RemoveUnusedVariablesPass(ac, fn).run_pass()

iszeros = [inst for inst in bb.instructions if inst.opcode == "iszero"]
removed_iszeros = iszero_count - len(iszeros)

assert removed_iszeros % 2 == 0
assert len(iszeros) in [1, 2]
assert len(iszeros) % 2 == iszero_count % 2


@pytest.mark.parametrize("interleave_point", range(1, 5))
def test_interleaved_case(interleave_point):
iszeros_after_interleave_point = interleave_point // 2
ctx = IRContext()
fn = ctx.create_function("_global")

bb = fn.get_basic_block()

br1 = IRBasicBlock(IRLabel("then"), fn)
fn.append_basic_block(br1)
br2 = IRBasicBlock(IRLabel("else"), fn)
fn.append_basic_block(br2)

p1 = bb.append_instruction("param")
op1 = bb.append_instruction("store", p1)
op2 = bb.append_instruction("store", 64)
op3 = bb.append_instruction("add", op1, op2)
op3_inv = bb.append_instruction("iszero", op3)
jnz_input = op3_inv
for _ in range(interleave_point):
jnz_input = bb.append_instruction("iszero", jnz_input)
bb.append_instruction("mstore", jnz_input, p1)
for _ in range(iszeros_after_interleave_point):
jnz_input = bb.append_instruction("iszero", jnz_input)
bb.append_instruction("jnz", jnz_input, br1.label, br2.label)

br1.append_instruction("add", op3, 10)
br1.append_instruction("stop")
br2.append_instruction("add", op3, p1)
br2.append_instruction("stop")

ac = IRAnalysesCache(fn)
MakeSSA(ac, fn).run_pass()
AlgebraicOptimizationPass(ac, fn).run_pass()
RemoveUnusedVariablesPass(ac, fn).run_pass()

assert bb.instructions[-1].opcode == "jnz"
if (interleave_point + iszeros_after_interleave_point) % 2 == 0:
assert bb.instructions[-1].operands[0] == op3_inv
else:
assert bb.instructions[-1].operands[0] == op3
2 changes: 2 additions & 0 deletions vyper/venom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from vyper.venom.context import IRContext
from vyper.venom.function import IRFunction
from vyper.venom.ir_node_to_venom import ir_node_to_venom
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.make_ssa import MakeSSA
Expand Down Expand Up @@ -50,6 +51,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None:
SCCP(ac, fn).run_pass()
StoreElimination(ac, fn).run_pass()
SimplifyCFGPass(ac, fn).run_pass()
AlgebraicOptimizationPass(ac, fn).run_pass()
BranchOptimizationPass(ac, fn).run_pass()
RemoveUnusedVariablesPass(ac, fn).run_pass()
DFTPass(ac, fn).run_pass()
Expand Down
4 changes: 4 additions & 0 deletions vyper/venom/analysis/dfg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional

from vyper.venom.analysis.analysis import IRAnalysesCache, IRAnalysis
from vyper.venom.analysis.liveness import LivenessAnalysis
from vyper.venom.basicblock import IRInstruction, IRVariable
from vyper.venom.function import IRFunction

Expand Down Expand Up @@ -67,5 +68,8 @@ def as_graph(self) -> str:
lines.append("}")
return "\n".join(lines)

def invalidate(self):
self.analyses_cache.invalidate_analysis(LivenessAnalysis)

def __repr__(self) -> str:
return self.as_graph()
2 changes: 1 addition & 1 deletion vyper/venom/basicblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ def is_terminal(self) -> bool:
return len(self.cfg_out) == 0

@property
def in_vars(self) -> OrderedSet[IRVariable]:
def liveness_in_vars(self) -> OrderedSet[IRVariable]:
for inst in self.instructions:
if inst.opcode != "phi":
return inst.liveness
Expand Down
67 changes: 67 additions & 0 deletions vyper/venom/passes/algebraic_optimization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from vyper.venom.analysis.dfg import DFGAnalysis
from vyper.venom.analysis.liveness import LivenessAnalysis
from vyper.venom.basicblock import IRInstruction, IROperand
from vyper.venom.passes.base_pass import IRPass


class AlgebraicOptimizationPass(IRPass):
"""
This pass reduces algebraic evaluatable expressions.
It currently optimizes:
* iszero chains
"""

def _optimize_iszero_chains(self) -> None:
fn = self.function
for bb in fn.get_basic_blocks():
for inst in bb.instructions:
if inst.opcode != "iszero":
continue

iszero_chain = self._get_iszero_chain(inst.operands[0])
iszero_count = len(iszero_chain)
if iszero_count == 0:
continue

for use_inst in self.dfg.get_uses(inst.output):
opcode = use_inst.opcode

if opcode == "iszero":
# We keep iszero instuctions as is
continue
if opcode in ("jnz", "assert"):
# instructions that accept a truthy value as input:
# we can remove up to all the iszero instructions
keep_count = 1 - iszero_count % 2
else:
# all other instructions:
# we need to keep at least one or two iszero instructions
keep_count = 1 + iszero_count % 2

if keep_count >= iszero_count:
continue

out_var = iszero_chain[keep_count].operands[0]
use_inst.replace_operands({inst.output: out_var})

def _get_iszero_chain(self, op: IROperand) -> list[IRInstruction]:
chain: list[IRInstruction] = []

while True:
inst = self.dfg.get_producing_instruction(op)
if inst is None or inst.opcode != "iszero":
break
op = inst.operands[0]
chain.append(inst)

chain.reverse()
return chain

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

self._optimize_iszero_chains()

self.analyses_cache.invalidate_analysis(DFGAnalysis)
self.analyses_cache.invalidate_analysis(LivenessAnalysis)
6 changes: 5 additions & 1 deletion vyper/venom/passes/make_ssa.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def run_pass(self):

self.analyses_cache.request_analysis(CFGAnalysis)
self.dom = self.analyses_cache.request_analysis(DominatorTreeAnalysis)

# Request liveness analysis so the `liveness_in_vars` field is valid
self.analyses_cache.request_analysis(LivenessAnalysis)

self._add_phi_nodes()
Expand All @@ -28,6 +30,8 @@ def run_pass(self):
self._rename_vars(fn.entry)
self._remove_degenerate_phis(fn.entry)

self.analyses_cache.invalidate_analysis(LivenessAnalysis)

def _add_phi_nodes(self):
"""
Add phi nodes to the function.
Expand All @@ -54,7 +58,7 @@ def _add_phi_nodes(self):
defs.append(dom)

def _place_phi(self, var: IRVariable, basic_block: IRBasicBlock):
if var not in basic_block.in_vars:
if var not in basic_block.liveness_in_vars:
return

args: list[IROperand] = []
Expand Down
1 change: 0 additions & 1 deletion vyper/venom/passes/mem2var.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class Mem2Var(IRPass):
def run_pass(self):
self.analyses_cache.request_analysis(CFGAnalysis)
dfg = self.analyses_cache.request_analysis(DFGAnalysis)
self.analyses_cache.request_analysis(LivenessAnalysis)

self.var_name_count = 0
for var, inst in dfg.outputs.items():
Expand Down
3 changes: 3 additions & 0 deletions vyper/venom/passes/remove_unused_variables.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from vyper.utils import OrderedSet
from vyper.venom.analysis.dfg import DFGAnalysis
from vyper.venom.analysis.liveness import LivenessAnalysis
from vyper.venom.basicblock import IRInstruction
from vyper.venom.passes.base_pass import IRPass

Expand All @@ -25,6 +26,8 @@ def run_pass(self):
inst = work_list.pop()
self._process_instruction(inst)

self.analyses_cache.invalidate_analysis(LivenessAnalysis)

def _process_instruction(self, inst):
if inst.output is None:
return
Expand Down

0 comments on commit 2b93d79

Please sign in to comment.