-
Notifications
You must be signed in to change notification settings - Fork 49
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
Detect and reroll unrolled do-while loops #76
base: master
Are you sure you want to change the base?
Conversation
This is in a stable state. The scope doesn't need to get much bigger for this PR; fixing &&/|| detection and more complex do-while detection can be in different PRs. |
So far this diff comprises:
|
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 great - I really like how it's relatively small/readable it feels for what it's doing.
for s in n.children(): | ||
if s.loop is not None and n in s.loop.backedges: | ||
continue | ||
incoming_edges[s].remove(n) | ||
try: | ||
incoming_edges[s].remove(n) | ||
except KeyError: | ||
break |
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.
At first glance, I think the break
should be continue
(if it's OK to have missing edges) or return False
(if it's not OK) -- otherwise, right now the behavior seems dependent on the order of the children in the list?
I think the fix here might actually be to iterate over set(children)
instead. If n
isn't in incoming_edges[s]
, then I think that means s.parents
was not constructed correctly? (Sorry that this might be a pre-existing bug)
for s in set(n.children()):
if s.loop is not None and n in s.loop.backedges:
continue
incoming_edges[s].remove(n)
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 that mean n.children()
might have repeats? That makes sense actually, and is probably why my extremely old version of this PR didn't need this fix (I changed children()
to a Set
.)
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 wait, I'm wrong, my bad!
n.children()
shouldn't have repeats: the implementations for that fn in SwitchNode
& ConditionalNode
explicitly deduplicate the children.
I can take a look and figure out what's going on here.
if changed: | ||
break | ||
compute_relations(flow_graph.nodes) | ||
return flow_graph |
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 modifies flow_graph
in-place, so I don't think it needs to return anything.
src/loop_rerolling.py
Outdated
node_2 = node_1.fallthrough_edge | ||
node_5 = node_1.conditional_edge | ||
|
||
if not isinstance(node_2, BasicNode): |
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 should also check that these nodes have the parents we expect. I feel like this pass has a large potential for misdetection and changing the asm into something non-equivalent, so we should be as careful as we can in the pattern-matching.
src/loop_rerolling.py
Outdated
replace_node_references(flow_graph, replace_this, with_this) | ||
|
||
|
||
def reroll_loop(flow_graph: FlowGraph, start: ConditionalNode) -> 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.
Add a comment with the structure/asm we're trying to pattern-match here (or refer to a test?).
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'd be nice to include a dot
representation of what's going on:
digraph before {
start -> node_1 [label=F, color=blue];
start -> node_7 [label=T, color=red];
node_1 -> node_2 [label=F, color=blue];
node_1 -> node_5 [label=T, color=red];
node_2 -> node_3;
node_3 -> node_4 [label=F, color=blue];
node_3 -> node_3 [label=T, color=red];
node_4 -> node_5 [label=F, color=blue];
node_4 -> node_7 [label=T, color=red];
node_5 -> node_6;
node_6 -> node_7 [label=F, color=blue];
node_6 -> node_6 [label=T, color=red];
}
digraph after {
start -> new_node_1 [label=F, color=blue];
start -> node_7 [label=T, color=red];
new_node_1 -> node_2 [label=F, color=blue];
new_node_1 -> node_7 [label=T, color=red];
node_2 -> node_3;
node_3 -> node_7 [label=F, color=blue];
node_3 -> node_3 [label=T, color=red];
}
And maybe a link to an online render, like https://bit.ly/3A2MEJt
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 actually nearly exactly what the new reroll.py does -- stay tuned
indices = [node.block.index for node in flow_graph.nodes] | ||
assert sorted(indices) == indices, "FlowGraphs should be sorted" | ||
|
||
offset = start.block.index |
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.
start
may not be equal to flow_graph.nodes[start.block.index]
: maybe instead use flow_graph.nodes.index(start)
(and catch the error & return None
)?
Right now this can happen when ReturnNode
s are duplicated, but unrolling loops would also mess up the block indexing.
return tuple( | ||
node | ||
for i, node in enumerate(flow_graph.nodes) | ||
if node is not start and i in all_nodes_in_pattern |
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 don't think start
should be excluded?
If you include start
in the output, then when you call nodes = detect_pattern(...)
, nodes[i]
refers to the node i
in the pattern. (Without it, nodes[0]
refers to node 1, etc.)
return tuple( | ||
node | ||
for i, node in enumerate(flow_graph.nodes) | ||
if node is not start and i in all_nodes_in_pattern | ||
) |
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.
return tuple( | |
node | |
for i, node in enumerate(flow_graph.nodes) | |
if node is not start and i in all_nodes_in_pattern | |
) | |
return tuple(flow_graph.nodes[i] for i in sorted(all_nodes_in_pattern)) |
def remove_and_replace_nodes(flow_graph: FlowGraph, nodes: Tuple[Node, ...]) -> None: | ||
(node_1, node_2, node_3, node_4, node_5, node_6, node_7) = nodes | ||
new_node_1 = BasicNode(node_1.block, node_1.emit_goto, node_2) | ||
replace_node(flow_graph, node_1, new_node_1) | ||
remove_node(flow_graph, node_4, node_7) | ||
remove_node(flow_graph, node_5, node_7) | ||
remove_node(flow_graph, node_6, node_7) # TODO: assert didn't execute anything?. |
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 is only used once, and looks pretty specific to the exact structure: I think it'd be easier if the body were just inlined directly in reroll_loop
?
# TODO: Check that the subgraph is self-contained except for the entry | ||
# and exit nodes, which themselves should be somehow "designated" using an enum. |
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 the following check is sufficient, without requiring you to explicitly mark an exit node in the pattern? (I think an exit node is any node that is not in pattern.keys()
but is in pattern.values()
? And the start node is 0
?)
I think it can be up to the caller to check start.parents
: I don't think the start's parents matter in this case.
# pattern_nodes could be the `tuple(flow_graph.nodes[i] for i in sorted(all_nodes_in_pattern))` return value I suggest later
for node in pattern_nodes:
if node is not start and not all(p in pattern_nodes for p in node.parents):
return None
Don't merge this yet - just putting some WIP work up.