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

Detect and reroll unrolled do-while loops #76

Open
wants to merge 59 commits into
base: master
Choose a base branch
from
Open

Conversation

matt-kempster
Copy link
Owner

Don't merge this yet - just putting some WIP work up.

@matt-kempster matt-kempster changed the title [WIP] Detect for and while loops and avoid &&/|| crashes Detect and reroll unrolled do-while loops May 1, 2020
@matt-kempster
Copy link
Owner Author

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.

@matt-kempster
Copy link
Owner Author

So far this diff comprises:

  • reroll.py, which needs to be refactored so that it doesn't suck
    • I have some local code which can help this, namely regarding replacing graphs with subgraphs in an automated way
  • changes to translate.py that are ostensibly all bugs and need to be diagnosed and fixed
  • adding --no-reroll as a CLI arg
  • adding replace_any_children() to BaseNode: is this acceptable?
  • adding a test that doesn't really need to be there (I should remove it)

Copy link
Collaborator

@zbanks zbanks left a 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.

src/flow_graph.py Outdated Show resolved Hide resolved
src/flow_graph.py Outdated Show resolved Hide resolved
Comment on lines 1359 to +1365
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
Copy link
Collaborator

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)

Copy link
Owner Author

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.)

Copy link
Collaborator

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.

src/translate.py Outdated Show resolved Hide resolved
src/loop_rerolling.py Outdated Show resolved Hide resolved
if changed:
break
compute_relations(flow_graph.nodes)
return flow_graph
Copy link
Collaborator

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/flow_graph.py Outdated Show resolved Hide resolved
src/translate.py Outdated Show resolved Hide resolved
tests/end_to_end/loop_nested/irix-o2-out.c Show resolved Hide resolved
src/loop_rerolling.py Outdated Show resolved Hide resolved
src/loop_rerolling.py Outdated Show resolved Hide resolved
node_2 = node_1.fallthrough_edge
node_5 = node_1.conditional_edge

if not isinstance(node_2, BasicNode):
Copy link
Collaborator

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.

replace_node_references(flow_graph, replace_this, with_this)


def reroll_loop(flow_graph: FlowGraph, start: ConditionalNode) -> bool:
Copy link
Collaborator

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?).

Copy link
Collaborator

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

Copy link
Owner Author

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
Copy link
Collaborator

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 ReturnNodes 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
Copy link
Collaborator

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.)

Comment on lines +80 to +84
return tuple(
node
for i, node in enumerate(flow_graph.nodes)
if node is not start and i in all_nodes_in_pattern
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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))

Comment on lines +87 to +93
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?.
Copy link
Collaborator

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?

Comment on lines +72 to +73
# TODO: Check that the subgraph is self-contained except for the entry
# and exit nodes, which themselves should be somehow "designated" using an enum.
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants