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

Simplify control flow for CFGs generated by eggcc. #610

Merged
merged 12 commits into from
May 30, 2024

Conversation

ezrosent
Copy link
Collaborator

There's a fairly detailed doc comment in simplify_branches.rs. The goal here is to clean up the CFG output and remove redundant branches from the generated code. Here's an example: at head, the sqrt benchmark generates the following cfg:

sqrt-old

And with this PR, we get:

sqrt-new

@ezrosent
Copy link
Collaborator Author

Question for reviewers: this will remove more branches if we iterate to a fixed point. It's easy to add; let me know if you want to try it.

@ezrosent
Copy link
Collaborator Author

Went ahead and added that in the next commit. Figure it's worth looking at nightly with everything. It's a small change and easy to revert. Here's the result for the same sqrt program.
sqrt-fp

@ezrosent ezrosent requested a review from oflatt May 26, 2024 21:53
@ezrosent ezrosent marked this pull request as ready for review May 26, 2024 21:53
Copy link
Member

@oflatt oflatt left a comment

Choose a reason for hiding this comment

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

Really great job, excited that this is working!
I think some of the gnarly checks that actually do the graph rewrites can be cleaned up. The actual analysis seems pretty clean.

src/rvsdg/simplify_branches.rs Show resolved Hide resolved
#[derive(Clone, Default, Debug)]
enum ValueInfo {
#[default]
Bot,
Copy link
Member

Choose a reason for hiding this comment

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

Worth commenting intuition about Bot and Top. I think Bot is "knows nothing" and Top is unreachable because we found a conflict

src/rvsdg/simplify_branches.rs Show resolved Hide resolved
Transform,
)>,
/// The set of variables written to in this basic block.
kills: HashSet<Identifier>,
Copy link
Member

Choose a reason for hiding this comment

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

As far as I can tell this is the variables written to in transforms. Could we just initialize it after step 2?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can, but then if we added more calls to add_assignment later, we'd then have to remember to recompute this. I think it's a bit less brittle to keep something like the current structure.

src/rvsdg/simplify_branches.rs Outdated Show resolved Hide resolved
continue;
};
let Some(val) = to_lit(&val) else {
continue;
Copy link
Member

Choose a reason for hiding this comment

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

If we keep the continue and break statements, might be worth labeling the while loops

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When I see labelled while loops I generally assume that some code is going to do a multi-level break... I think the continues and breaks here are pretty clear? Are we doing this elsewhere in the code?

let Some(val) = to_lit(&val) else {
continue;
};
if val_analysis.data[admin_node].kills.contains(&arg) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this if is a special, easy case- can it be broken out into a helper? Or a whole different pass perhaps?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The thing that makes that harder is the break vs. continue case. We can lift that into its own type, but that + a bunch of params creates a bit of an odd helper. I think this might be better but I'm not sure.

As for a separate pass I think that'd require that we redo a bunch of the scaffolding for the while loops? Not sure if that's worth it either.

    enum Next { Break, Continue }
    fn fold_constant_branch(
        &mut self,
        admin_node: NodeIndex,
        succ: NodeIndex,
        arg: &Identifier,
        val_analysis: &ValueAnalysis,
        val: &Literal,
    ) -> (Next, bool) {
        if succ != self.exit && self.graph.neighbors(admin_node).any(|x| x == self.exit) {
            // Don't remove any outgoing links to the exit node.
            return (Next::Break, false);
        }
        // We assign to the branched-on argument in the admin
        // node. See if we can fold the constant branch here.
        let ValueInfo::Known(lit) = val_analysis.data[&admin_node].get_output(arg) else {
            return (Next::Continue, false);
        };
        if &lit != val {
            return (Next::Continue, false);
        }
        // okay, we have found a matching edge. Replace this branch
        // with a jump.
        let mut walker = self
            .graph
            .neighbors_directed(admin_node, Direction::Outgoing)
            .detach();
        while let Some((outgoing, _)) = walker.next(&self.graph) {
            self.graph.remove_edge(outgoing);
        }
        self.graph.add_edge(
            admin_node,
            succ,
            Branch {
                op: BranchOp::Jmp,
                pos: None,
            },
        );
        // Don't run the rest of the inner loop.
        (Next::Break, true)
}

Copy link
Member

Choose a reason for hiding this comment

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

I agree the helper is weird
I still think a separate while loop may be cleaner but I'm not hung up on this

src/rvsdg/simplify_branches.rs Outdated Show resolved Hide resolved
// checked that the footer was empty when we populated
// admin_nodes. We do this because we more or less don't
// use footers on our way back to bril.
scratch.extend(self.graph[*admin_node].instrs.iter().cloned());
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I understand how scratch is getting used. Can scratch be a local definition here? What about just above this while loop instead of far up?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

scratch is being used to save repeated allocations when running the algorithm, so the higher up the better I think.

I've moved it inside rewrite_branches though.

Copy link
Member

Choose a reason for hiding this comment

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

Oh I see.
I think scratch is a bad name, and I don't care about the repeated allocations.

src/rvsdg/simplify_branches.rs Outdated Show resolved Hide resolved
Copy link
Member

@oflatt oflatt left a comment

Choose a reason for hiding this comment

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

Really great improvement! This looks ready to merge, it's challenging to refactor further as we've seen from the threads

@ezrosent ezrosent merged commit 516a106 into egraphs-good:main May 30, 2024
2 checks passed
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.

2 participants