Skip to content

Commit

Permalink
refactor[venom]: introduce IRContext and IRAnalysisCache (#3983)
Browse files Browse the repository at this point in the history
this is a refactoring commit. it introduces `IRContext` and
`IRAnalysisCache`. `IRContext` is a global compilation context for a
compilation run, it includes global information like the deploy- and
runtime-code. meanwhile, `IRFunction` now corresponds directly to
"internal" functions passed from the frontend.

this commit refactors the `IRAnalysis` and `IRPass` classes by adding
`IRAnalysisCache` (kind of a manager for analyses) which determines
which analyses are needed for a given pass.

additionally, there is a compilation-time performance improvement which
is due to optimizations running over smaller sections of code.

this commit introduces some performance regressions due to
`remove_unused_variables` running in one-shot instead of in a loop now.
these performance regressions will be investigated and tuned in a
follow-up PR.
  • Loading branch information
harkal authored May 4, 2024
1 parent 533b271 commit ace3789
Show file tree
Hide file tree
Showing 31 changed files with 865 additions and 837 deletions.
4 changes: 3 additions & 1 deletion tests/unit/compiler/venom/test_convert_basicblock_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ def test_simple():
venom = ir_node_to_venom(ir_node)
assert venom is not None

bb = venom.basic_blocks[0]
fn = list(venom.functions.values())[0]

bb = fn.entry
assert bb.instructions[0].opcode == "calldatasize"
assert bb.instructions[1].opcode == "calldatacopy"

Expand Down
46 changes: 24 additions & 22 deletions tests/unit/compiler/venom/test_dominator_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

from vyper.exceptions import CompilerPanic
from vyper.utils import OrderedSet
from vyper.venom.analysis import calculate_cfg
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.analysis.dominators import DominatorTreeAnalysis
from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IRLiteral, IRVariable
from vyper.venom.dominators import DominatorTree
from vyper.venom.context import IRContext
from vyper.venom.function import IRFunction
from vyper.venom.passes.make_ssa import MakeSSA


def _add_bb(
ctx: IRFunction, label: IRLabel, cfg_outs: [IRLabel], bb: Optional[IRBasicBlock] = None
fn: IRFunction, label: IRLabel, cfg_outs: list[IRLabel], bb: Optional[IRBasicBlock] = None
) -> IRBasicBlock:
bb = bb if bb is not None else IRBasicBlock(label, ctx)
ctx.append_basic_block(bb)
bb = bb if bb is not None else IRBasicBlock(label, fn)
fn.append_basic_block(bb)
cfg_outs_len = len(cfg_outs)
if cfg_outs_len == 0:
bb.append_instruction("stop")
Expand All @@ -29,27 +30,27 @@ def _add_bb(
def _make_test_ctx():
lab = [IRLabel(str(i)) for i in range(0, 9)]

ctx = IRFunction(lab[1])
ctx = IRContext()
fn = ctx.create_function(lab[1].value)

bb1 = ctx.basic_blocks[0]
bb1.append_instruction("jmp", lab[2])
fn.entry.append_instruction("jmp", lab[2])

_add_bb(ctx, lab[7], [])
_add_bb(ctx, lab[6], [lab[7], lab[2]])
_add_bb(ctx, lab[5], [lab[6], lab[3]])
_add_bb(ctx, lab[4], [lab[6]])
_add_bb(ctx, lab[3], [lab[5]])
_add_bb(ctx, lab[2], [lab[3], lab[4]])
_add_bb(fn, lab[7], [])
_add_bb(fn, lab[6], [lab[7], lab[2]])
_add_bb(fn, lab[5], [lab[6], lab[3]])
_add_bb(fn, lab[4], [lab[6]])
_add_bb(fn, lab[3], [lab[5]])
_add_bb(fn, lab[2], [lab[3], lab[4]])

return ctx
return fn


def test_deminator_frontier_calculation():
ctx = _make_test_ctx()
bb1, bb2, bb3, bb4, bb5, bb6, bb7 = [ctx.get_basic_block(str(i)) for i in range(1, 8)]
fn = _make_test_ctx()
bb1, bb2, bb3, bb4, bb5, bb6, bb7 = [fn.get_basic_block(str(i)) for i in range(1, 8)]

calculate_cfg(ctx)
dom = DominatorTree.build_dominator_tree(ctx, bb1)
ac = IRAnalysesCache(fn)
dom = ac.request_analysis(DominatorTreeAnalysis)
df = dom.dominator_frontiers

assert len(df[bb1]) == 0, df[bb1]
Expand All @@ -62,12 +63,13 @@ def test_deminator_frontier_calculation():


def test_phi_placement():
ctx = _make_test_ctx()
bb1, bb2, bb3, bb4, bb5, bb6, bb7 = [ctx.get_basic_block(str(i)) for i in range(1, 8)]
fn = _make_test_ctx()
bb1, bb2, bb3, bb4, bb5, bb6, bb7 = [fn.get_basic_block(str(i)) for i in range(1, 8)]

x = IRVariable("%x")
bb1.insert_instruction(IRInstruction("mload", [IRLiteral(0)], x), 0)
bb2.insert_instruction(IRInstruction("add", [x, IRLiteral(1)], x), 0)
bb7.insert_instruction(IRInstruction("mstore", [x, IRLiteral(0)]), 0)

MakeSSA().run_pass(ctx, bb1)
ac = IRAnalysesCache(fn)
MakeSSA(ac, fn).run_pass()
7 changes: 4 additions & 3 deletions tests/unit/compiler/venom/test_duplicate_operands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from vyper.compiler.settings import OptimizationLevel
from vyper.venom import generate_assembly_experimental
from vyper.venom.function import IRFunction
from vyper.venom.context import IRContext


def test_duplicate_operands():
Expand All @@ -15,8 +15,9 @@ def test_duplicate_operands():
Should compile to: [PUSH1, 10, DUP1, DUP1, DUP1, ADD, MUL, STOP]
"""
ctx = IRFunction()
bb = ctx.get_basic_block()
ctx = IRContext()
fn = ctx.create_function("test")
bb = fn.get_basic_block()
op = bb.append_instruction("store", 10)
sum_ = bb.append_instruction("add", op, op)
bb.append_instruction("mul", sum_, op)
Expand Down
32 changes: 16 additions & 16 deletions tests/unit/compiler/venom/test_make_ssa.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
from vyper.venom.analysis import calculate_cfg, calculate_liveness
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.basicblock import IRBasicBlock, IRLabel
from vyper.venom.function import IRFunction
from vyper.venom.context import IRContext
from vyper.venom.passes.make_ssa import MakeSSA


def test_phi_case():
ctx = IRFunction(IRLabel("_global"))
ctx = IRContext()
fn = ctx.create_function("_global")

bb = ctx.get_basic_block()
bb = fn.get_basic_block()

bb_cont = IRBasicBlock(IRLabel("condition"), ctx)
bb_then = IRBasicBlock(IRLabel("then"), ctx)
bb_else = IRBasicBlock(IRLabel("else"), ctx)
bb_if_exit = IRBasicBlock(IRLabel("if_exit"), ctx)
ctx.append_basic_block(bb_cont)
ctx.append_basic_block(bb_then)
ctx.append_basic_block(bb_else)
ctx.append_basic_block(bb_if_exit)
bb_cont = IRBasicBlock(IRLabel("condition"), fn)
bb_then = IRBasicBlock(IRLabel("then"), fn)
bb_else = IRBasicBlock(IRLabel("else"), fn)
bb_if_exit = IRBasicBlock(IRLabel("if_exit"), fn)
fn.append_basic_block(bb_cont)
fn.append_basic_block(bb_then)
fn.append_basic_block(bb_else)
fn.append_basic_block(bb_if_exit)

v = bb.append_instruction("mload", 64)
bb_cont.append_instruction("jnz", v, bb_then.label, bb_else.label)
Expand All @@ -30,11 +31,10 @@ def test_phi_case():

bb.append_instruction("jmp", bb_cont.label)

calculate_cfg(ctx)
MakeSSA().run_pass(ctx, ctx.basic_blocks[0])
calculate_liveness(ctx)
ac = IRAnalysesCache(fn)
MakeSSA(ac, fn).run_pass()

condition_block = ctx.get_basic_block("condition")
condition_block = fn.get_basic_block("condition")
assert len(condition_block.instructions) == 2

phi_inst = condition_block.instructions[0]
Expand Down
102 changes: 55 additions & 47 deletions tests/unit/compiler/venom/test_multi_entry_block.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
from vyper.venom.analysis import calculate_cfg
from vyper.venom.function import IRBasicBlock, IRFunction, IRLabel
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.analysis.cfg import CFGAnalysis
from vyper.venom.context import IRContext
from vyper.venom.function import IRBasicBlock, IRLabel
from vyper.venom.passes.normalization import NormalizationPass


def test_multi_entry_block_1():
ctx = IRFunction()
ctx = IRContext()
fn = ctx.create_function("__global")

finish_label = IRLabel("finish")
target_label = IRLabel("target")
block_1_label = IRLabel("block_1", ctx)
block_1_label = IRLabel("block_1", fn)

bb = ctx.get_basic_block()
bb = fn.get_basic_block()
op = bb.append_instruction("store", 10)
acc = bb.append_instruction("add", op, op)
bb.append_instruction("jnz", acc, finish_label, block_1_label)

block_1 = IRBasicBlock(block_1_label, ctx)
ctx.append_basic_block(block_1)
block_1 = IRBasicBlock(block_1_label, fn)
fn.append_basic_block(block_1)
acc = block_1.append_instruction("add", acc, op)
op = block_1.append_instruction("store", 10)
block_1.append_instruction("mstore", acc, op)
block_1.append_instruction("jnz", acc, finish_label, target_label)

target_bb = IRBasicBlock(target_label, ctx)
ctx.append_basic_block(target_bb)
target_bb = IRBasicBlock(target_label, fn)
fn.append_basic_block(target_bb)
target_bb.append_instruction("mul", acc, acc)
target_bb.append_instruction("jmp", finish_label)

finish_bb = IRBasicBlock(finish_label, ctx)
ctx.append_basic_block(finish_bb)
finish_bb = IRBasicBlock(finish_label, fn)
fn.append_basic_block(finish_bb)
finish_bb.append_instruction("stop")

calculate_cfg(ctx)
assert not ctx.normalized, "CFG should not be normalized"
ac = IRAnalysesCache(fn)
ac.request_analysis(CFGAnalysis)
assert not fn.normalized, "CFG should not be normalized"

NormalizationPass().run_pass(ctx)
NormalizationPass(ac, fn).run_pass()

assert ctx.normalized, "CFG should be normalized"
assert fn.normalized, "CFG should be normalized"

finish_bb = ctx.get_basic_block(finish_label.value)
finish_bb = fn.get_basic_block(finish_label.value)
cfg_in = list(finish_bb.cfg_in)
assert cfg_in[0].label.value == "target", "Should contain target"
assert cfg_in[1].label.value == "__global_split_finish", "Should contain __global_split_finish"
Expand All @@ -47,91 +51,95 @@ def test_multi_entry_block_1():

# more complicated one
def test_multi_entry_block_2():
ctx = IRFunction()
ctx = IRContext()
fn = ctx.create_function("__global")

finish_label = IRLabel("finish")
target_label = IRLabel("target")
block_1_label = IRLabel("block_1", ctx)
block_2_label = IRLabel("block_2", ctx)
block_1_label = IRLabel("block_1", fn)
block_2_label = IRLabel("block_2", fn)

bb = ctx.get_basic_block()
bb = fn.get_basic_block()
op = bb.append_instruction("store", 10)
acc = bb.append_instruction("add", op, op)
bb.append_instruction("jnz", acc, finish_label, block_1_label)

block_1 = IRBasicBlock(block_1_label, ctx)
ctx.append_basic_block(block_1)
block_1 = IRBasicBlock(block_1_label, fn)
fn.append_basic_block(block_1)
acc = block_1.append_instruction("add", acc, op)
op = block_1.append_instruction("store", 10)
block_1.append_instruction("mstore", acc, op)
block_1.append_instruction("jnz", acc, target_label, finish_label)

block_2 = IRBasicBlock(block_2_label, ctx)
ctx.append_basic_block(block_2)
block_2 = IRBasicBlock(block_2_label, fn)
fn.append_basic_block(block_2)
acc = block_2.append_instruction("add", acc, op)
op = block_2.append_instruction("store", 10)
block_2.append_instruction("mstore", acc, op)
# switch the order of the labels, for fun and profit
block_2.append_instruction("jnz", acc, finish_label, target_label)

target_bb = IRBasicBlock(target_label, ctx)
ctx.append_basic_block(target_bb)
target_bb = IRBasicBlock(target_label, fn)
fn.append_basic_block(target_bb)
target_bb.append_instruction("mul", acc, acc)
target_bb.append_instruction("jmp", finish_label)

finish_bb = IRBasicBlock(finish_label, ctx)
ctx.append_basic_block(finish_bb)
finish_bb = IRBasicBlock(finish_label, fn)
fn.append_basic_block(finish_bb)
finish_bb.append_instruction("stop")

calculate_cfg(ctx)
assert not ctx.normalized, "CFG should not be normalized"
ac = IRAnalysesCache(fn)
ac.request_analysis(CFGAnalysis)
assert not fn.normalized, "CFG should not be normalized"

NormalizationPass().run_pass(ctx)
NormalizationPass(ac, fn).run_pass()

assert ctx.normalized, "CFG should be normalized"
assert fn.normalized, "CFG should be normalized"

finish_bb = ctx.get_basic_block(finish_label.value)
finish_bb = fn.get_basic_block(finish_label.value)
cfg_in = list(finish_bb.cfg_in)
assert cfg_in[0].label.value == "target", "Should contain target"
assert cfg_in[1].label.value == "__global_split_finish", "Should contain __global_split_finish"
assert cfg_in[2].label.value == "block_1_split_finish", "Should contain block_1_split_finish"


def test_multi_entry_block_with_dynamic_jump():
ctx = IRFunction()
ctx = IRContext()
fn = ctx.create_function("__global")

finish_label = IRLabel("finish")
target_label = IRLabel("target")
block_1_label = IRLabel("block_1", ctx)
block_1_label = IRLabel("block_1", fn)

bb = ctx.get_basic_block()
bb = fn.get_basic_block()
op = bb.append_instruction("store", 10)
acc = bb.append_instruction("add", op, op)
bb.append_instruction("djmp", acc, finish_label, block_1_label)

block_1 = IRBasicBlock(block_1_label, ctx)
ctx.append_basic_block(block_1)
block_1 = IRBasicBlock(block_1_label, fn)
fn.append_basic_block(block_1)
acc = block_1.append_instruction("add", acc, op)
op = block_1.append_instruction("store", 10)
block_1.append_instruction("mstore", acc, op)
block_1.append_instruction("jnz", acc, finish_label, target_label)

target_bb = IRBasicBlock(target_label, ctx)
ctx.append_basic_block(target_bb)
target_bb = IRBasicBlock(target_label, fn)
fn.append_basic_block(target_bb)
target_bb.append_instruction("mul", acc, acc)
target_bb.append_instruction("jmp", finish_label)

finish_bb = IRBasicBlock(finish_label, ctx)
ctx.append_basic_block(finish_bb)
finish_bb = IRBasicBlock(finish_label, fn)
fn.append_basic_block(finish_bb)
finish_bb.append_instruction("stop")

calculate_cfg(ctx)
assert not ctx.normalized, "CFG should not be normalized"
ac = IRAnalysesCache(fn)
ac.request_analysis(CFGAnalysis)
assert not fn.normalized, "CFG should not be normalized"

NormalizationPass().run_pass(ctx)
assert ctx.normalized, "CFG should be normalized"
NormalizationPass(ac, fn).run_pass()
assert fn.normalized, "CFG should be normalized"

finish_bb = ctx.get_basic_block(finish_label.value)
finish_bb = fn.get_basic_block(finish_label.value)
cfg_in = list(finish_bb.cfg_in)
assert cfg_in[0].label.value == "target", "Should contain target"
assert cfg_in[1].label.value == "__global_split_finish", "Should contain __global_split_finish"
Expand Down
Loading

0 comments on commit ace3789

Please sign in to comment.