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]: add loop invariant hoisting pass #4175

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
13944eb
feat[venom]: add loop invariant hoisting pass
HodanPlodky Jul 10, 2024
f626b68
feat[venom]: add unit test for loop invariant hoisting
HodanPlodky Jul 12, 2024
942c731
feat[venom]: add unit test for loop invariant hoisting
HodanPlodky Jul 12, 2024
399a0a4
feat[venom]: clean up and comments
HodanPlodky Jul 15, 2024
2a8dd4a
fix[venom]: incorrect loop detection fix
HodanPlodky Jul 15, 2024
302aa21
fix[venom]: removed hoisting just constants
HodanPlodky Jul 17, 2024
013840c
fix[venom]: removed unnecessary hoisting
HodanPlodky Jul 18, 2024
1ee0068
feat[venom]: clean up after changes
HodanPlodky Jul 18, 2024
f703408
fix[venom]: changes from review and better loophoisting structure (sk…
HodanPlodky Jul 19, 2024
c5dbf05
fix[venom]: ignore code offset calculation
HodanPlodky Jul 22, 2024
cd655fc
fix[venom]: changes according to review
HodanPlodky Jul 22, 2024
ecb272a
fix[venom]: changes according to review
HodanPlodky Jul 22, 2024
18c7610
fix[venom]: changes according to review
HodanPlodky Jul 23, 2024
5583cc5
fix[venom]: review changes (_is_store condition negation to reduce ne…
HodanPlodky Jul 23, 2024
bdb2896
clean up loop detection
harkal Jul 29, 2024
788bd0d
Merge pull request #1 from harkal/update_loop_detection
HodanPlodky Aug 10, 2024
715b128
fix[venom]: improved tests to increase coverage
HodanPlodky Aug 12, 2024
8edce11
fix[venom]: error when the store is not constant
HodanPlodky Aug 13, 2024
cf6b25e
Merge branch 'master' into loop_invariant
HodanPlodky Sep 30, 2024
77f97b6
Merge branch 'master' into loop_invariant
HodanPlodky Jan 9, 2025
e62c8cd
Merge branch 'master' into loop_invariant
HodanPlodky Jan 31, 2025
139f79b
mypy help
HodanPlodky Feb 1, 2025
029af38
Merge branch 'master' into loop_invariant
HodanPlodky Feb 25, 2025
28414a9
Merge branch 'master' into loop_invariant
HodanPlodky Feb 27, 2025
556bbbb
effect handling
HodanPlodky Feb 27, 2025
83ce93c
allow to hoist effectful instructions
HodanPlodky Feb 27, 2025
78e2074
start of test rewrite and lint
HodanPlodky Feb 28, 2025
a8336da
simple test
HodanPlodky Feb 28, 2025
013c7c1
removed unused function
HodanPlodky Feb 28, 2025
e367b98
tests
HodanPlodky Mar 3, 2025
4e91eac
unhoistable body
HodanPlodky Mar 7, 2025
6dde829
lint
HodanPlodky Mar 7, 2025
11e50ce
Merge branch 'master' into loop_invariant
HodanPlodky Mar 11, 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
229 changes: 229 additions & 0 deletions tests/unit/compiler/venom/test_loop_invariant_hoisting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import pytest

from tests.venom_utils import assert_ctx_eq, parse_from_basic_block
from vyper.venom.analysis.analysis import IRAnalysesCache
from vyper.venom.analysis.loop_detection import NaturalLoopDetectionAnalysis
from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRVariable
from vyper.venom.function import IRFunction
from vyper.venom.passes.loop_invariant_hosting import LoopInvariantHoisting


def _helper_reorder(fn: IRFunction):
for bb in fn.get_basic_blocks():
bb.instructions.sort(key=lambda inst: repr(inst))

Check notice

Code scanning / CodeQL

Unnecessary lambda Note test

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.


def _create_loops(fn, depth, loop_id, body_fn=lambda _: (), top=True):
bb = fn.get_basic_block()
cond = IRBasicBlock(IRLabel(f"cond{loop_id}{depth}"), fn)
body = IRBasicBlock(IRLabel(f"body{loop_id}{depth}"), fn)
if top:
exit_block = IRBasicBlock(IRLabel(f"exit_top{loop_id}{depth}"), fn)
else:
exit_block = IRBasicBlock(IRLabel(f"exit{loop_id}{depth}"), fn)
fn.append_basic_block(cond)
fn.append_basic_block(body)

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

cond_var = IRVariable(f"cond_var{loop_id}{depth}")
cond.append_instruction("iszero", 0, ret=cond_var)
assert isinstance(cond_var, IRVariable)
cond.append_instruction("jnz", cond_var, body.label, exit_block.label)
body_fn(fn, loop_id, depth)
if depth > 1:
_create_loops(fn, depth - 1, loop_id, body_fn, top=False)
bb = fn.get_basic_block()
bb.append_instruction("jmp", cond.label)
fn.append_basic_block(exit_block)


def _hoistable_body(loop_id, depth):
return f"""
%{loop_id}{depth} = 1
%a0{loop_id}{depth} = add %{loop_id}{depth}, 1
%a1{loop_id}{depth} = add %a0{loop_id}{depth}, %{loop_id}{depth}
"""


def _simple_body(loop_id, depth):
return f"""
%a{depth}{loop_id} = add 1, 2"""


def _create_loops_code(depth, loop_id, body=lambda _, _a: "", last: bool = False):
if depth <= 0:
return ""
inner = _create_loops_code(depth - 1, loop_id, body, False)

res = f"""
jmp @cond{depth}{loop_id}
cond{depth}{loop_id}:
jnz %par, @exit{depth}{loop_id}, @body{depth}{loop_id}
body{depth}{loop_id}:
{body(loop_id, depth)}
{inner}
jmp @cond{depth}{loop_id}
exit{depth}{loop_id}:
"""

if last:
res += """
stop
"""

return res


@pytest.mark.parametrize("depth", range(1, 4))
@pytest.mark.parametrize("count", range(1, 4))
def test_loop_detection_analysis(depth, count):
loops = ""
for i in range(count):
loops += _create_loops_code(depth, i, _simple_body, last=(i == count - 1))

code = f"""
main:
%par = param
{loops}
"""

print(code)

ctx = parse_from_basic_block(code)
assert len(ctx.functions) == 1

fn = list(ctx.functions.values())[0]
ac = IRAnalysesCache(fn)
analysis = ac.request_analysis(NaturalLoopDetectionAnalysis)
assert isinstance(analysis, NaturalLoopDetectionAnalysis)

assert len(analysis.loops) == depth * count


@pytest.mark.parametrize("depth", range(1, 4))
@pytest.mark.parametrize("count", range(1, 4))
def test_loop_invariant_hoisting_simple(depth, count):
pre_loops = ""
for i in range(count):
pre_loops += _create_loops_code(depth, i, _simple_body, last=(i == count - 1))

post_loops = ""
for i in range(count):
hoisted = ""
for d in range(depth):
hoisted += _simple_body(i, depth - d)
post_loops += hoisted
post_loops += _create_loops_code(depth, i, last=(i == count - 1))

pre = f"""
main:
%par = param
{pre_loops}
"""

post = f"""
main:
%par = param
{post_loops}
"""

ctx = parse_from_basic_block(pre)
print(ctx)

for fn in ctx.functions.values():
ac = IRAnalysesCache(fn)
LoopInvariantHoisting(ac, fn).run_pass()
_helper_reorder(fn)

post_ctx = parse_from_basic_block(post)

for fn in post_ctx.functions.values():
ac = IRAnalysesCache(fn)

Check notice

Code scanning / CodeQL

Unused local variable Note test

Variable ac is not used.
_helper_reorder(fn)

print(ctx)
print(post_ctx)

assert_ctx_eq(ctx, post_ctx)


@pytest.mark.parametrize("depth", range(1, 4))
@pytest.mark.parametrize("count", range(1, 4))
def test_loop_invariant_hoisting_dependant(depth, count):
pre_loops = ""
for i in range(count):
pre_loops += _create_loops_code(depth, i, _hoistable_body, last=(i == count - 1))

post_loops = ""
for i in range(count):
hoisted = ""
for d in range(depth):
hoisted += _hoistable_body(i, depth - d)
post_loops += hoisted
post_loops += _create_loops_code(depth, i, last=(i == count - 1))

pre = f"""
main:
%par = param
{pre_loops}
"""

post = f"""
main:
%par = param
{post_loops}
"""

ctx = parse_from_basic_block(pre)
print(ctx)

for fn in ctx.functions.values():
ac = IRAnalysesCache(fn)
LoopInvariantHoisting(ac, fn).run_pass()
_helper_reorder(fn)

post_ctx = parse_from_basic_block(post)

for fn in post_ctx.functions.values():
_helper_reorder(fn)

print(ctx)
print(post_ctx)

assert_ctx_eq(ctx, post_ctx)


def _unhoistable_body(loop_id, depth):
return f"""
%l{loop_id}{depth} = mload 64
%a{loop_id}{depth} = add 2, %l{loop_id}{depth}
mstore %a{loop_id}{depth}, 10
"""


@pytest.mark.parametrize("depth", range(1, 4))
@pytest.mark.parametrize("count", range(1, 4))
def test_loop_invariant_hoisting_unhoistable(depth, count):
pre_loops = ""
for i in range(count):
pre_loops += _create_loops_code(depth, i, _unhoistable_body, last=(i == count - 1))

pre = f"""
main:
%par = param
{pre_loops}
"""

ctx = parse_from_basic_block(pre)
print(ctx)

for fn in ctx.functions.values():
ac = IRAnalysesCache(fn)
LoopInvariantHoisting(ac, fn).run_pass()

print(ctx)

orig = parse_from_basic_block(pre)

assert_ctx_eq(ctx, orig)
2 changes: 2 additions & 0 deletions vyper/venom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
StoreElimination,
StoreExpansionPass,
)
from vyper.venom.passes.loop_invariant_hosting import LoopInvariantHoisting
from vyper.venom.venom_to_assembly import VenomCompiler

DEFAULT_OPT_LEVEL = OptimizationLevel.default()
Expand Down Expand Up @@ -84,6 +85,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache

AlgebraicOptimizationPass(ac, fn).run_pass()
RemoveUnusedVariablesPass(ac, fn).run_pass()
LoopInvariantHoisting(ac, fn).run_pass()

StoreExpansionPass(ac, fn).run_pass()

Expand Down
69 changes: 69 additions & 0 deletions vyper/venom/analysis/loop_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from vyper.utils import OrderedSet
from vyper.venom.analysis.analysis import IRAnalysis
from vyper.venom.analysis.cfg import CFGAnalysis
from vyper.venom.basicblock import IRBasicBlock


class NaturalLoopDetectionAnalysis(IRAnalysis):
"""
Detects loops and computes basic blocks
and the block which is before the loop
"""

# key = loop header
# value = all the blocks that the loop contains
loops: dict[IRBasicBlock, OrderedSet[IRBasicBlock]]

def analyze(self):
self.analyses_cache.request_analysis(CFGAnalysis)
self.loops = self._find_natural_loops(self.function.entry)

# Could possibly reuse the dominator tree algorithm to find the back edges
# if it is already cached it will be faster. Still might need to separate the
# varius extra information that the dominator analysis provides
# (like frontiers and immediate dominators)
def _find_back_edges(self, entry: IRBasicBlock) -> list[tuple[IRBasicBlock, IRBasicBlock]]:
back_edges = []
visited: OrderedSet[IRBasicBlock] = OrderedSet()
stack = []

def dfs(bb: IRBasicBlock):
visited.add(bb)
stack.append(bb)

for succ in bb.cfg_out:
if succ not in visited:
dfs(succ)
elif succ in stack:
back_edges.append((bb, succ))

stack.pop()

dfs(entry)

return back_edges

def _find_natural_loops(
self, entry: IRBasicBlock
) -> dict[IRBasicBlock, OrderedSet[IRBasicBlock]]:
back_edges = self._find_back_edges(entry)
natural_loops = {}

for u, v in back_edges:
# back edge: u -> v
loop: OrderedSet[IRBasicBlock] = OrderedSet()
stack = [u]

while stack:
bb = stack.pop()
if bb in loop:
continue
loop.add(bb)
for pred in bb.cfg_in:
if pred != v:
stack.append(pred)

loop.add(v)
natural_loops[v.cfg_in.first()] = loop

return natural_loops
Loading
Loading