RFC: Add "call unwindabort" to LLVM IR

I see, so you quoted and are advocating for the third rejected alternative, right? I share the concerns listed in the original RFC.

The plan is to add an unwindabort marker to the resume instruction for these cases. I think it’s precisely equivalent to what you wrote (call unwindabort @_Unwind_Resume), but modifying the resume instruction, rather than creating a call to a known API.

The most I can say in support of the landingpad-only approach is that I have heard some folks complain about LLVM’s “superblock” design, where control can exit the function mid-basic block. It has been proposed that all real function calls should end the basic block.

However, that’s not what we have today. Part of the advantage of the unwindabort strategy is that we get closer to optimizer parity between -fno-exceptions and a hypothetical “noexcept everywhere” compilation mode. The optimizer will see the same set of basic blocks. Adding imaginary landingpads that get pattern matched away later looks different to the optimizer, and part of the goal is to make code compiled these two ways optimize the same way.

Until this moment, I hadn’t even considered how this affects PGO. With this representation, it means the CFG is much more likely to match across the transition from noexceptions to noexcept, even if it can’t be guaranteed.

Hmm. This would only be usable if there isn’t any need for a landing pad at that call site, though, right? In which case we’d also have the more compact option of just leaving the call site uncovered by the table.

Ideally what we’re looking for is some way to encode that the Nth action at a call site is to just call terminate. And I guess we need the personality function to not terminate unless it would actually select that action, although maybe that works out implicitly since the personality function presumably doesn’t look at actions until it’s decided that nothing else applies.

Well, there are three concerns there.

I can’t argue with the first concern, that it’s easier to optimize single basic blocks. I do think that this is a bit of a dodge, though, because we really shouldn’t be writing optimizations that just give up at any sign of control flow. Usually it’s not difficult to make an optimization capable of reasoning with exits from an otherwise linear CFG, which is enough to let it walk over a typical invoke. (And of course a single-BB optimization in LLVM still does need to reason about exits within the block.)

I don’t find the second concern very convincing, that an unreachable landing pad is weird.

The third concern seems to be overlooking something important, which is that we do need to put something representing this into the landingpad unless we are actually going to prevent inlining an invoke with handlers into a call unwindabort. call unwindabort represents a non-trivial EH scope, and whenever you inline into a non-trivial EH scope, the EH actions of the resulting call must represent both scopes. For an EH action that’s just cleanups, we can magically do this merge by just dropping the cleanups and turning the original invoke into a call unwindabort, but what’s the plan for when the inner invoke has catches or filters?

That’s a good point. As usual, inlining EH actions through a call is the truly interesting case, and this is where LLVM would inline the resume and mark it unwindabort. You are saying we need some marker on the landingpad to stop searching for outer EH handlers. Today this is handled with a catch-all + terminate from the frontend, and the inliner could plausibly add a catch-all clause.

Alternatively, maybe we could add a cleanup clause to the landingpad. This ensures that if some handler is found, the program will terminate the program at the inlined resume. If no handler is found, you get the behavior of an unhandled exception. I seem to recall there are separate hooks for that, so perhaps that’s not functionally equivalent.

The last alternative is I guess what you are exploring, which is a new kind of entry in the LSDA.

Regarding superblocks vs invoke-unwind-to-unreachable, I’m not trying to make a statement about what the LLVM optimizer should aspire to do. I agree LLVM should optimize across blocks. But I am observing that more LLVM optimizations will fire today with combined blocks, and one of the goals of James’s proposed -fno-exceptions=noexcept mode is for it to have performance parity with regular -fno-exceptions. This eases migration to a language mode with less UB.

Alternatively, we can ask the question: could we model all potentially unwinding calls with invoke-to-unreachable under fno-exceptions? Would our users let us ship that? I think the answer is “no”, it would be infeasible to fix all of the performance regressions in that mode, even if it’s a good goal for the LLVM optimizer. But, I could be wrong.

I’m not going to try to insist on my point about single-block optimization; I know that it’s abstract philosophy, that we have short-term needs here, and that (to be frank) the community has next to no interest in putting any effort towards achieving optimization parity for multi-exit block sequences.

I would consider my objections satisfied if we can define call unwindabort as a shorthand (with better optimization properties) for something expressible with invoke + landingpad. That should be as simple as adding an unwindabort action to landingpad, currently listed as a future direction in the RFC above. Then the only issue is the ABI / deployment problem of expressing that action in its full generality, i.e. when it needs to be combined with other actions. While I am generally in favor of making the frontend responsible[1] for emitting IR that can be implemented on the target, in this case, because this handle-else-abort pattern can be constructed in the middle-end via inlining, that seems like the wrong approach. Instead, we should support the feature unconditionally in IR, emit it freely in the frontend, and then find some way to handle it in the backend. Assuming we don’t come up with a magical LSDA encoding that triggers termination on all current runtimes, I suggest:

  1. If personality functions add a way to encode this, the frontend can enable the use of that with some sort of global metadata on the module.
  2. In the absence of that metadata, unwindabort in the landingpad becomes a catch-all if it isn’t the only action (other than cleanup), and resume unwindabort is compiled to something compatible with that.
  3. In the presence of that metadata, unwindabort in the landingpad is emitted in the new way, and resume unwindabort can be compiled as unreachable.

  1. really, acknowledging the frontend’s responsibility ↩︎

Thanks, I think that is workable, but I know James has more to say and is trying to find the time for it :slight_smile:

1 Like

Ideally what we’re looking for is some way to encode that the Nth action at a call site is to just call terminate. And I guess we need the personality function to not terminate unless it would actually select that action, although maybe that works out implicitly since the personality function presumably doesn’t look at actions until it’s decided that nothing else applies.

How about either a catch or exception spec with a fake typeinfo shim in it that calls terminate when you call its can_catch()? Since we have control over what goes in the body of that typeinfo shim on the compiler side, we can construct whatever arbitrary behavior we need (I think?), and all implementations must de-facto respect it as a matter of it already being part of the ABI.

Yeah, that should be possible, and it has the virtue of working with any existing runtime today.

Downsides off the top of my head:

  • We’d have to actually emit that fake RTTI in any translation unit that might need it. That’d be a little annoying, especially because the compiler doesn’t normally need to know the type_info v-table layout. (For RTTI emission, it just emits references to known v-tables provided by the stdlib and doesn’t need to understand the layout of the v-table at all.) I don’t think is an insurmountable problem, though.

  • The personality function only seems to consider handlers (other than the special null encoding for catch-alls) when the thrown exception is native. So a special RTTI is just going to let a non-native exception (e.g. from GNU Java) sail right on through the frame. This might be a serious blocker — I don’t think this is a common combination, but irrevocably breaking it seems really bad. (I don’t think this would be a problem for ObjC, though — Darwin’s ObjC ABI presents its exceptions as native C++ exceptions, just exceptions that use their own type_info class defined in the ObjC runtime.)

  • It would be weird to use a weird RTTI object in a non-C++ personality function like __gcc_personality_v0 (normally just used for __attribute__((cleanup)) in C) because those aren’t supposed to know anything about C++. If we want the noexcept enforcement mode to be usable in non-C++ translation units, that’s a serious blocker. On the other hand, __gcc_personality_v0 doesn’t even try to interpret an action table, so to use the enforcement mode in C, we will need a new runtime regardless.

  • Finally, the personality function wouldn’t be able to detect those fake type_info objects if it ever had need to. We’d have to leave some sort of breadcrumb for this, like a special type name; it might be something we’d regret.


The other possibility that comes to mind is that we could put an invalid type_info pointer in the LSDA, like ((std::type_info*) 1). The obvious virtue here is that it doesn’t require emitting any extra globals. Also, it might well be the encoding we’d want to use if we just extended the LSDA ABI, so it’s reasonable to ask if we could just get away with using it now.

So, new runtimes would be able to recognize this easily and trigger a terminate. Old runtimes wouldn’t recognize it, so they’d try to use it, and on most platforms that would reliably crash. That crash would terminate the process, presumably, and it would do so without actually unwinding first, since the crash would happen in unwind phase 1.

Downsides I see:

  • Technically, the crash is undefined behavior. This doesn’t seem like a huge issue, actually — the UB in the personality function is reliably compiled separately from user code, so it’s not going to be optimized in any way that could trigger non-local problems, so we can analyze what the compiler will do to the existing runtime code is isolation. And of course the plan is that future versions of the runtime won’t have the UB anyway.

  • There could be platforms where there isn’t a pointer we could use that would reliably crash. This isn’t much of a problem for “mainstream” platforms, and non-mainstream platforms are more likely to be using a controlled toolchain where we can get them using an updated runtime that knows about the special pointer.

  • It crashes instead of calling std::terminate, which means it violates the spec until someone updates their runtime, which they can’t necessarily do themselves. This is pretty bad.

  • Existing versions of libc++abi will think it’s a catch handler for a native C++ type, so it has the same problem as the bogus RTTI approach that it won’t be considered for foreign thrown exceptions. (libsupc++ seems to have slightly different behavior here; I didn’t dig too deep.)


Considering our options here, I think we should probably just give up and use a catch-all action unless we know we’re targeting a runtime that supports the new encoding, whatever that ends up being. (Of course, we can continue to drop the call site or use a null landing pad if the terminate action doesn’t have to be combined with other actions.)

OK – I’ve rebased the patches pushed earlier, and will start addressing comments on those. Catching up on comments on this thread…

My plan was that when the inner invoke has catches or filters, then the invoke is preserved, and:

  1. The landingpad is modified to add a cleanup clause, if not already present. This ensures that all exceptions thrown within the call are caught here [But only in phase2. Thus, if no outer catch exists, the terminate will occur from the phase1 search failing to find a handler. If there is an outer catch, terminate will occur from this cleanup]
  2. All resume are switched to resume unwindabort, which will result in the desired termination after the cleanup is handled.

Yes, this does mean we don’t get the improvements of unwindabort (e.g. giving a full backtrace), when this triggers. But, that’s as good as we can get without ABI changes.

I definitely think it would be a good future step to add an unwindabort clause to landingpad, once we have a way to implement it. But, I’d really prefer to avoid blocking the rest of this on agreeing upon that piece. My attention is back on this project now, and I’d like to not lose the momentum. :slight_smile:

I do not want to implement the landingpad clause when we cannot represent it anywhere, and do not even know yet how exactly the underlying ABI change will look. If we implement it now, it would have to function identically to cleanup. It will be extra complexity added, to no current effect, with the hope that when we do implement the unwindabort-clause in landingpad for real, the changes we made in the meantime are useful for that.

I believe it’ll be cleaner to teach the inliner to create a “landingpad unwindabort” only if that is known to be supported, and to otherwise use the transformation I described above.

This is effectively what someone has implemented in GitHub - m42a/gcc-noexcept-plugin (after my comment on 55918 – Stack partially unwound when noexcept causes call to std::terminate). However, the downsides described by rjmccall’s reply here seem significant.

I also note there was some side-discussion on GCC 97720 – throw in try in noexcept fn calls terminate without handling the exception – proposing to potentially add a new personality function for use in C++17 and later (from which there’s no longer a need to support filter clauses), that can re-purpose a negative number in the action-table to mean “terminate”. Not sure if that’s the best way, but it does seem like one viable option.

1 Like

This is a reasonable plan (that wasn’t in the RFC!), and it’s a good point that we should compile to a cleanup in the action table instead of a catchall so that we don’t unwind if the exception would otherwise be unhandled. (I’m not sure this is strictly correct in case of resumptive EH, but I’m fine with ignoring that.)

I’m still not sure I see why we shouldn’t try to represent this faithfully in the IR for as long as possible.

As I mentioned before, we’re eager to try this for Meta’s Android apps for code size. I commented on ⚙ D141918 WIP: [Clang] Emit 'unwindabort' when applicable.; there’s a crash with coroutines that’s preventing us from fully building our apps with the change, unfortunately.

The parts of the app that I am able to build are actually seeing a slight size increase, weirdly enough. The .eh_frame section gets substantially larger, which doesn’t make sense to me … we wouldn’t be adding any unwind info to functions that didn’t already have it prior to this change, right? .gcc_except_table is much smaller, as expected, but the number of GCC_except_table symbols increases by a lot (without any corresponding increase in other symbols), which is even weirder. I’m still digging into what’s going on there, but we’ll also be able to get much more accurate numbers once coroutines are working :slight_smile:

@jyknight as we discussed at the LLVM developer meeting, my team at Meta is very interested in seeing this work land. In particular, we recently had to enable exceptions across a large code base that I work on, so a better noexcept representation is even more important for us now to mitigate exception cost wherever possible. Please let us know any way we can help move this forward (code reviews, any bits we can contribute ourselves, etc.).