[LLVMdev] C++ exception handling

Kaylor, Andrew andrew.kaylor at intel.com
Fri Feb 13 15:52:54 PST 2015


OK then.  I’ll proceed as I was, trusting that we can come up with something to handle the nightmare cases before it’s finished.

Thanks,
Andy


From: Reid Kleckner [mailto:rnk at google.com]
Sent: Friday, February 13, 2015 3:47 PM
To: Kaylor, Andrew
Cc: David Majnemer; LLVM Developers Mailing List
Subject: Re: C++ exception handling



On Fri, Feb 13, 2015 at 3:28 PM, Kaylor, Andrew <andrew.kaylor at intel.com<mailto:andrew.kaylor at intel.com>> wrote:
I’m not sure I understand what you are suggesting in terms of how this should be handled.

I agree that the cleanup code is unlikely to be complex, but obviously we still need some way to handle any cases that unlikely but possible.  I hadn’t even thought about what might be in a __finally block.  I was thinking about destructors being inlined.  Obviously that puts a practical limit on what will be there, but not really a theoretical limit.

So I’m looking for an implementation that will handle the typical cases efficiently but still not break if the very rare case turns up.  It occurs to me that maybe the depth first search I was planning is exactly wrong in this case.  Given a cleanup handler that goes into a shared block and can either branch back out to a block that belongs to the landing pad or branch to a block at some arbitrary point in the normal flow, the path we’re looking for is almost certainly going to meet the selector comparison or a resume within a block or two.  So a breadth-first search would handle this case better and probably is reasonable for the common case (which usually won’t have any branches at all).

There are two scenarios I have in mind here.

1) A shared block that might branch into normal code:

lpad:
  landingpad …
    cleanup
    catch …
  …
  br label %shared.block

shared.block:
  %dst = phi i1 [1, %normal.cont], [0, %lpad]
  ; do something that happens in cleanup and in regular code
  …
  br i1 %dst, label %normal.cont2, label %catch.dispatch

catch.dispatch:

That sort of PHI dependence is very easy to handle, but can we count on it always being that simple?  It seems like in theory there could be an arbitrary use-def chain between the PHI node and the branch condition that we may or may not be able to resolve at compile-time.  I wouldn’t be happy with whoever created such an atrocity, but I didn’t feel like we could just assume it can’t happen.

If we follow the branch to “normal.cont2” it could conceivably take us through the entire CFG for the function.  Obviously we want to avoid that.

In the case where we have such unanalyzable code, I think outlining the entire function is a good way of being conservatively correct. We also have tools that help us here. For example we know that 'ret' is unreachable in a cleanup. Cleanup code must return to either 'resume' or more EH dispatch. We can propagate that backwards and delete unreachable code.

That said, it might be more practical to just report_fatal_error when cleanup code doesn't rejoin EH dispatch. Outlining the whole function will generate terrible, terrible code, and I'd rather tweak the frontend and intervening optimizers to avoid this situation.

2) A block shared between unconnected landing pads

lpad1:
  landingpad …
    cleanup
    catch <some type> …
  …
  br label %shared.block

lpad2:
  landingpad …
    cleanup
    catch <some other type> …
  …
  br label %shared.block

shared.block:
  %dst = phi i1 [1, %lpad1], [0, %lpad2]
  ; do something that happens in both cleanup handlers
  …
  br i1 %dst, label %lpad1.catch.dispatch, label %lpad2.catch.dispatch

lpad1.catch.dispatch:
…

lpad2.catch.dispatch:
…

I haven’t tried to devise code that would lead to this, but my intuition is that this kind of sharing is more likely than the other and if it does happen it could be problematic.  Again, the PHI-dependence here is easy to sort out here and it would be trivial to determine which branch we should follow.  But if a case arises where we can’t figure out which predecessor leads to which successor, what I had planned just won’t work.

I’d like to believe that all PHI-dependent control flow that arises from block merging will always be simple and that if anyone tried to do something more complex someone would ask them not to, but I worry that you never know what is going to happen over the course of many transformation passes.  Like, imagine:

shared.block:
  %foo = phi i1 [1, %lpad1], [0, %lpad2]
  …
  %fubar = call i1 @bar(i1 %foo)
  …
  br i1 %fubar, label %dispatch1, label %dispatch2

Am I overthinking this?

I want to say that transforms should never do this, but it's better to be correct in the face of bad code. =/

When I talked through this with my coworkers, everyone kind of calmed down when we realized that cloning the whole function body was essentially a conservatively correct outlining implementation.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20150213/55cba242/attachment.html>


More information about the llvm-dev mailing list