-
-
Notifications
You must be signed in to change notification settings - Fork 816
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
base: master
Are you sure you want to change the base?
Conversation
def analyze(self): | ||
self.analyses_cache.request_analysis(CFGAnalysis) | ||
self.loops: dict[IRBasicBlock, list[IRBasicBlock]] = dict() | ||
done: dict[IRBasicBlock, bool] = dict( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually use a set
for this purpose, and you will see that we also tent to use our own OrderedSet
implementation even when really not visibly necessary, to avoid ending up with some weird non-deterministic code generation.
def invalidate(self): | ||
return super().invalidate() | ||
|
||
def dfs(self, bb: IRBasicBlock, done: dict[IRBasicBlock, bool], path: list[IRBasicBlock]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't this done
be promoted to a class member?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could. Should it be promoted even though it is only used for dfs
method and is reset before every run of analyze
and is not necessary for consumer of this analysis?
from vyper.venom.basicblock import IRBasicBlock | ||
|
||
|
||
class LoopDetection(IRAnalysis): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a convention we end all analysis names in Analysis
self.dfs(entry) | ||
|
||
def invalidate(self): | ||
return super().invalidate() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for this implementation
def invalidate(self): | ||
return super().invalidate() | ||
|
||
def dfs(self, bb: IRBasicBlock, before: IRBasicBlock = None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function does loop detection, why name it dfs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: we use the convention foo_r()
for recursive functions, and prefix with underscore (i.e. _foo_r()
) for "private" functions (those that are not called externally)
self.done.add(bb) | ||
return | ||
|
||
def collect_path(self, bb_from: IRBasicBlock, bb_to: IRBasicBlock) -> OrderedSet[IRBasicBlock]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: _collect_path
self.collect_path_inner(bb_from, bb_to, loop, collect_visit) | ||
return loop | ||
|
||
def collect_path_inner( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_collect_path_inner()
def _get_hoistable_loop( | ||
self, from_bb: IRBasicBlock, loop: OrderedSet[IRBasicBlock] | ||
) -> list[tuple[IRBasicBlock, IRBasicBlock, IRInstruction]]: | ||
result: list[tuple[IRBasicBlock, IRBasicBlock, IRInstruction]] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these type signatures are hard to read -- either make them less specific (e.g. -> list
or -> list[tuple]
) or make a small dataclass local to this file to carry around the data structure.
return True | ||
return False | ||
|
||
def _can_hoist_instruction_ignore_stores( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leave a comment explaining why this is necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe just call this _can_hoist_instruction
(or just _can_hoist
!), as it doesn't need to be contrasted with a function like _can_hoist_instruction_not_ignoring_stores
.
for in_var in instruction.get_input_variables(): | ||
assert isinstance(in_var, IRVariable) | ||
source_ins = self.dfg._dfg_outputs[in_var] | ||
if source_ins in bb.instructions: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we can just check inst.parent
for each instruction here
break | ||
# I have this inside the loop because I dont need to | ||
# invalidate if you dont hoist anything | ||
self.analyses_cache.invalidate_analysis(LivenessAnalysis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's bring this outside the loop, and add a flag inside the loop to detect if invalidate_analysis
should be called
def run_pass(self): | ||
self.analyses_cache.request_analysis(CFGAnalysis) | ||
self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) | ||
loops = self.analyses_cache.request_analysis(LoopDetectionAnalysis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loops.loops
looks weird, maybe just name this t
? or loops_analysis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loops_analysis
👍
Any VOLATILE_INSTRUCTIONS, BB_TERMINATORS CFG_ALTERING_INSTRUCTIONS are ignored | ||
""" | ||
|
||
from typing import Iterator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for local import, please hoist this to top of file
def _is_correct_store(instruction: IRInstruction) -> bool: | ||
return ( | ||
instruction.opcode == "store" | ||
and len(instruction.operands) == 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this condition is always true, no?
for in_var in instruction.get_input_variables(): | ||
assert isinstance(in_var, IRVariable) | ||
source_ins = self.dfg._dfg_outputs[in_var] | ||
if _is_correct_store(source_ins): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how can this condition ever hold true?
assert before is not None, "Loop must have one basic block before it" | ||
loop = self._collect_path(before, bb) | ||
in_bb = bb.cfg_in.difference({before}) | ||
assert len(in_bb) == 1, "Loop must have one input basic block" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Since we are looking for natural loops, we should probably rename the analysis to
NaturalLoopDetectionAnalysis
- We should also probably not assert if the loop was not natural, just ignore that loop
self._dfs_r(neighbour, bb) | ||
|
||
self.done.add(bb) | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redundant
) | ||
|
||
|
||
def _is_correct_store(instruction: IRInstruction) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically _is_store()
def _can_hoist_instruction_ignore_stores( | ||
self, instruction: IRInstruction, loop: OrderedSet[IRBasicBlock] | ||
) -> bool: | ||
if _ignore_instruction(instruction) or _is_correct_store(instruction): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_ignore_instruction()
includes testing for store
as it returns True
for volatiles. Just an _ignore_instructions()
is better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the volatiles are only sstore
, istore
, tstore
and mstore
, not store
. because of that I thought the check for store is necessary or did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh snap... you are right. I had that one named set
initially, never got over renaming it :D.. let's just tack that test in the _ignore_instruction()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I will add it there
return False | ||
return True | ||
|
||
def _dependant_in_bb(self, instruction: IRInstruction, bb: IRBasicBlock): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sp: dependent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this can be a free function rather than a method, as i don't see any references to self
in the body
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DFGAnalysis from self
def _dependant_in_bb(self, instruction: IRInstruction, bb: IRBasicBlock): | ||
for in_var in instruction.get_input_variables(): | ||
assert isinstance(in_var, IRVariable) | ||
source_ins = self.dfg._dfg_outputs[in_var] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefer instruction to always be abbreviated as inst
, and also to use the public interface of the dfg analysis, for example:
prev_inst = self.dfg.get_producing_instruction(in_var)
# can add `assert prev_inst is not None` if desired
self.analyses_cache.request_analysis(CFGAnalysis) | ||
self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) | ||
loops = self.analyses_cache.request_analysis(NaturalLoopDetectionAnalysis) | ||
self.loop_analysis = loops.loops |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not an analysis anymore so maybe it should just be self.loops = loops.loops
.
|
||
def _collect_path_r( | ||
self, | ||
act_bb: IRBasicBlock, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does "act_bb" stand for "active bb"? maybe cur_bb
or bb_from
or just bb
would be better.
act_bb: IRBasicBlock, | ||
bb_to: IRBasicBlock, | ||
loop: OrderedSet[IRBasicBlock], | ||
collect_visit: OrderedSet[IRBasicBlock], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe just visited
self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) | ||
loops = self.analyses_cache.request_analysis(NaturalLoopDetectionAnalysis) | ||
self.loop_analysis = loops.loops | ||
invalidate_dependant = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dependent what? maybe rename to just invalidate
for inst in hoistable: | ||
bb = inst.parent | ||
bb.remove_instruction(inst) | ||
target_bb.insert_instruction(inst, index=len(target_bb.instructions) - 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe index does not need to be specified in this case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am modifying already terminated basic block so I it would crash on assert if the index would not be specified
if before is None: | ||
return | ||
loop = self._collect_path(before, bb) | ||
in_bb = bb.cfg_in.difference({before}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this might be clearer as if before in bb.cfg_in: ...
. does that introduce any bugs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just because it checks for natural loops, so I check if it has only one input into the loop.
for var in inst.get_input_variables(): | ||
source_inst = self.dfg.get_producing_instruction(var) | ||
assert isinstance(source_inst, IRInstruction) | ||
if _is_store(source_inst): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reduce nesting:
if not _is_store(...):
continue
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4175 +/- ##
==========================================
- Coverage 91.34% 88.95% -2.40%
==========================================
Files 109 111 +2
Lines 15606 15758 +152
Branches 3432 3469 +37
==========================================
- Hits 14256 14018 -238
- Misses 920 1232 +312
- Partials 430 508 +78 ☔ View full report in Codecov by Sentry. |
What I did
Loop invariant hoisting
How I did it
Implemented loop detection analysis and IR pass which. The detection is done via dfs. The pass detects which instruction are not dependent on instruction within the loop and if not then the instruction is hoisted to the basic block before the loop. This is done iteratively until there are no such instruction in any loop
How to verify it
Commit message
feat[venom]: Loop invariant hoisting
Commit message for the final, squashed PR. (Optional, but reviewers will appreciate it! Please see our commit message style guide for what we would ideally like to see in a commit message.)
Description for the changelog
Cute Animal Picture