This is an archive of the discontinued LLVM Phabricator instance.

[WinEH] Teach SimplfyCFG to eliminate empty cleanup pads.
ClosedPublic

Authored by andrew.w.kaylor on Aug 28 2015, 9:01 AM.

Details

Summary

These changes introduce a new routine in SimplifyCFG to eliminate trivially empty cleanup pads from the exception handling chain.

There are a couple of places where it seemed like this could benefit from refactoring, but it seemed best for review purposes to simply note that and follow up after discussion of the basic implementation.

I also am making no attempt to handle PHI nodes. It seems unlikely that anything would put a PHI node at the top of an empty cleanup pad, but I don't know of anything that prevents it. This may need to be addressed later. Similarly, I think it's possible, but unlikely, for a lifetime intrinsic to appear in an otherwise empty cleanup pad.

Diff Detail

Repository
rL LLVM

Event Timeline

andrew.w.kaylor retitled this revision from to [WinEH] Teach SimplfyCFG to eliminate empty cleanup pads..
andrew.w.kaylor updated this object.
andrew.w.kaylor set the repository for this revision to rL LLVM.
andrew.w.kaylor added a subscriber: llvm-commits.

It seems unlikely that anything would put a PHI node at the top of an empty cleanup pad

What about something like

struct S { public: ~S() { } };
int foo() {
  int state = 1;
  try {
    S destructible;
    may_throw();
    state = 2;
    may_throw();
    // cleanup for S inserted here, which could be empty after inlining; wouldn't it get a phi for state?
  } catch (...) {
    return state;
  }
  return 0;
}

?

There are a couple of places where it seemed like this could benefit from refactoring, but it seemed best for review purposes to simply note that and follow up after discussion of the basic implementation.

I agree. Even in this same file, SimplifyUnreachable has redundant code for rewriting invoke -> call and probably should be extended to handle the EH pad -> 'unwind to caller' rewrites, and there's (going to be) a spot in WinEHPrepare where it would be useful to invoke this as a utility (see http://reviews.llvm.org/D12433#inline-100516 ).

lib/Transforms/Utils/SimplifyCFG.cpp
2965

Cleanups can have internal control flow, so this cast could fail. Presumably you want a dyn_cast and return false if it returns nullptr, since that would indicate a non-empty cleanup? (I'm not sure if this routine should be assuming that trivial flow will have already been removed or not)

test/Transforms/SimplifyCFG/empty-cleanuppad.ll
71–73

Should the CHECK-NOT follow the two CHECKs here instead of preceding them? In the input the cleanup comes after the catchendpad.

What about something like <snip> ?

Yeah, that does it. I guess I need to add support for that now.

lib/Transforms/Utils/SimplifyCFG.cpp
2965

Oops. I actually had the dyn_cast in there and then I removed it this morning as I was doing final cleanup of this code. That's what I get for trying to doing something before the morning caffeine kicks in.

test/Transforms/SimplifyCFG/empty-cleanuppad.ll
71–73

In this case the first cleanuppad sticks around and the one below the catchpad/catchret is removed, but I do still have this line misplaced. It should be below the catchendpad.

rnk added inline comments.Aug 31 2015, 2:15 PM
lib/Transforms/Utils/SimplifyCFG.cpp
2903–2904

We usually drop these kinds of utilities in llvm/lib/Transforms/Utils/Local.cpp. You can make llvm/Transforms/Utils/LowerInvoke.cpp use it too.

2906

2-space indent

andrew.w.kaylor edited edge metadata.

Addressed minor comments from previous reviews.
Added support for sinking PHI nodes.
Refactoring is still TBD.

lib/Transforms/Utils/SimplifyCFG.cpp
3056–3122

assert seems too strong here. Any PHI in an empty unwinds-to-caller cleanuppad must be dead (have no uses), but it would still be legal IR, right? I'd think that in the unwinds-to-caller case this should simply erase any PHIs it finds.

3069–3070

It could be a non-PHI value that dominates the empty cleanup, though, couldn't it? So something like

struct S { ... }; // has empty destructor
void foo() {
  int state = bar();
  try {
    {
      S s;
      may_throw();
    } // empty cleanup for s here
    state = 2;
    may_throw();
  } catch (...) {
    // This is UnwindDest and should have a PHI for state where IncomingBlock is the empty cleanuppad
    // and IncomingValue is the call to bar, not a PHI in the empty cleanup.
    code;
  }
}

or with a constant:

struct S { ... }; // has empty destructor
void foo() {
  int state = 1;
  try {
    {
      S s;
      may_throw();
    } // empty cleanup for s here
    state = 2;
    may_throw();
  } catch (...) {
    // This is UnwindDest and should have a PHI for state where IncomingBlock is the empty cleanuppad
    // and IncomingValue is 1, not a PHI in the empty cleanup
    code;
  }
}

Happily, I think in these cases you can just take the current incoming value in DestPN and associate it with UnwindDest's new predecessors.

3077–3080

I don't think the destination PHI node can already have an entry for this block:

  • SrcPN was a PHI in a cleanuppad, so SrcPN->getIncomingBlock(SrcIdx) must have reached the cleanuppad by an unwind edge
  • DestPN is in UnwindDest, which must be an EH pad and therefore all its predecessors must reach UnwindDest by an unwind edge
  • No instruction has multiple unwind edges, so nothing could have unwind edges to both UnwindDest and the empty cleanup
3085

I think it's possible that SrcPN could have another use on another PHI in UnwindDest, and/or that it could have a non-PHI use. So I don't think you want to erase it here, I think instead you want the loop below to check if it has any uses and erase it if not.

Actually, you may want to move the loop below outside the if(UnwindDest) and let it be the thing that erases useless PHIs when the empty cleanup unwinds to caller.

3100

Likewise, I think the getBasicBlockIndex check here is superfluous.

andrew.w.kaylor added inline comments.Sep 2 2015, 1:08 PM
lib/Transforms/Utils/SimplifyCFG.cpp
3056–3122

I suppose you're right. I think I can handle that just by replacing the assert with a comment. The unused PHI's should go away when we remove the block. Alternatively, I could add something to go through any PHI's and assert that they have no uses.

3069–3070

Good catch. I was just thinking it couldn't be a non-PHI value in the empty cleanup pad block. I think you're right that this should be easy to handle.

3077–3080

Alright, that seems like sound reasoning to me.

3085

OK.

3100

I'm assuming "likewise" here is referring to your comments at line 3069. So you're saying that any predecessor of the unwindpad we're removing could not also have been a predecessor of UnwindDest., right?

So, any PHINode that was present in the cleanuppad block we are removing must necessarily have undefined values for any other predecessor of UnwindDest. That sounds correct. Am I correct in thinking that because the PHI value from BB could not have been used in the pre-optimized control flow there must be something (a PHI-dependent branch, perhaps) still in place that will prevent the undef value being inserted here from being referenced?

JosephTremoulet added inline comments.Sep 2 2015, 2:51 PM
lib/Transforms/Utils/SimplifyCFG.cpp
3056–3122

The unused PHI's should go away when we remove the block.

Oh, I didn't know it worked like that. Feel free to ignore my suggestion to explicitly remove them then.

3100

So you're saying that any predecessor of the unwindpad we're removing could not also have been a predecessor of UnwindDest., right?

right.

Am I correct in thinking that because the PHI value from BB could not have been used in the pre-optimized control flow there must be something (a PHI-dependent branch, perhaps) still in place that will prevent the undef value being inserted here from being referenced?

Trying to come up with a justification for that claim, I instead convinced myself that you should actually be using the PHI itself as the incoming value, rather than undef:
You'll only insert a new incoming value if UnwindDest had some predecessor other than the empty cleanup.

  1. If the empty cleanup didn't dominate that other predecessor, then:
    • the empty cleanup (where the PHI used to be) can't dominate anything reachable from UnwindDest
    • then since UnwindDest was the empty cleanup's only successor, that means the empty cleanup didn't dominate anything but itself
    • and since the empty cleanup was empty, there weren't any uses there.
    • so in this case, the only place where the empty cleanup PHI could have had a use (because defs have to dominate uses) is a PHI in UnwindDef.
    • but if there was already a PHI in UnwindDef then it already had values for the other predecessors, so this isn't a case where you'll be inserting a new incoming value here.
    • so case 1 is contradictory and impossible.
  2. If the empty cleanup did dominate that other predecessor, then
    1. if the other predecessor is unreachable, then it doesn't really matter what you put there
    2. if the other predecessor is reachable, then its unwind edge is a back-edge, and since the original PHI wouldn't have changed value around that back-edge we need the new PHI to preserve its value around that back-edge.

So the only possible case is 2b, and the PHI needs a self-reference on the backedge. I don't think we can get that from C++ because it would have to look something like this and the 'goto' there is illegal:

struct S { ~S() { } };

void may_throw();
void use(int);

void foo() {
  int x = 1;
  try {
    {
      S s;
      may_throw();
      x = 2;
      may_throw();
    } // empty cleanup for S with phi for x here
    return;
  loop_bottom:
    may_throw();
    return;
  } catch (...) {
    // predecessors are the empty cleanup and loop_bottom
    // no phi here before optimization
    // phi for x will be moved here by optimization
    use(x); // use of the phi that gets moved
  }
  goto loop_bottom;
}

but at the LLVM IR level I don't see anything that would prohibit someone from creating

  define void foo() personality whatever {
entry:
    invoke void may_throw()
      to label %invoke.cont unwind label %cleanup
invoke.cont:
    invoke void may_throw()
      to label %exit unwind label %cleanup
cleanup:
  %x = PHI i32 [ 1, %entry ], [ 2, %invoke.cont ]
  %clean = cleanuppad []
  cleanupret %clean unwind label %catch
catch:
  %cat = catchpad []
      to label %do_catch unwind label %endcatch
do_catch:
  call void use(i32 %x)
  catchret %cat to label %loop_bottom
loop_bottom:
  invoke void f()
    to label %exit unwind label %catch
endcatch:
  catchendpad unwind to caller
exit:
  ret void
  }
andrew.w.kaylor added inline comments.Sep 2 2015, 2:58 PM
lib/Transforms/Utils/SimplifyCFG.cpp
3100

It turns out that by this point I've already updated the terminators for BB's predecessors so they are included in this loop. This check is necessary to distinguish between blocks that previous unwound to BB (and thus already have an entry in the PHI node that we are sinking) and blocks that unwound to UnwindDest even before the transformation (and thus need an undef entry in the PHI node we are sinking).

JosephTremoulet added inline comments.Sep 2 2015, 3:26 PM
lib/Transforms/Utils/SimplifyCFG.cpp
3100

It turns out that by this point I've already updated the terminators for BB's predecessors so they are included in this loop

Oh, I see. I think it would be good to avoid foreach(pred : ...) { PN->getBasicBlockIndex(pred) here if you can; it's quadratic and I'm certain to run into cases with EH pads that have several hundred preds. What would you think about swapping the order and doing the phi rewrite before the terminator rewrite?

JosephTremoulet added inline comments.Sep 3 2015, 3:38 AM
lib/Transforms/Utils/SimplifyCFG.cpp
3100

I'd think that jump threading could turn something like

struct S { ~S() { } };

void may_throw();
void use(int);

void foo() {
  int x = 1;
  bool doStuff = true;
  do {
   // phis for x and doStuff here
    try {
      // branch on doStuff phi that JumpThreading should be interested in, removing above phi on x
      if (doStuff) {
        S s;
        may_throw();
        x = 2;
        may_throw();
        // empty cleanup for S with phi for x here (incoming values are the loop-head phi and 2, but after jump-threading could be 1 and 2)
        return;
      }
      may_throw();
      return;
    } catch (...) {
      // phi here merging the loop-head phi with the empty-cleanup phi
      // after jump-threading this phi can be removed and have its use replaced with the empty-cleanup phi
      use(x);
    }
    doStuff = false;
  } while (true);
}

(which is legal C++) into the above pattern.

Update PHI handling.
Add new test case.

JosephTremoulet accepted this revision.Sep 3 2015, 12:49 PM
JosephTremoulet edited edge metadata.

LGTM with one nit, thanks.

lib/Transforms/Utils/SimplifyCFG.cpp
2993–2995

Looks like this doesn't need to be a loop anymore, and you can just have unsigned Idx = DestPN->getBasicBlockIndex(BB)

This revision is now accepted and ready to land.Sep 3 2015, 12:49 PM
andrew.w.kaylor edited edge metadata.

Eliminated redundant loop in PHI handling

Any objections to my committing this as is and doing the proposed refactoring as part of a separate change set?

One last nit.

Any objections to my committing this as is and doing the proposed refactoring as part of a separate change set?

No, that plan sounds good. In fact I checked in rL246751 with some code at line 3314 in WinEHPrepare.cpp that rewrites cleanupendpads as unreachable but instead ought to be calling this helper that the refactoring will create to change its predecessors to 'unwind to caller' (or from invoke to call).

lib/Transforms/Utils/SimplifyCFG.cpp
3046

I'm pretty sure you could assert Idx != -1; DestPN is a PHI node in block UnwindDest, which must have BB as a predecessor because it is the ->getUnwindDest() of BB's terminator.

This revision was automatically updated to reflect the committed changes.