Duncan Sands
2010-Dec-06 21:58 UTC
[LLVMdev] Inlining and exception handling in LLVM and GCC
The poor interaction between exception handling and inlining in LLVM is one of
the main motivations for the new exception handling models proposed recently.
Here I give my analysis of the origin of the problem in the hope of clarifying
the situation.
Soon after dwarf exception handling was implemented in LLVM, I noticed that some
programs would fail when compiled at -O3, for example the following:
#include <stdio.h>
struct X { ~X() { printf("Running destructor!\n"); } };
void foo() {
struct X x;
throw 1;
}
int main() {
try {
foo();
} catch (int) {
printf("Caught exception!\n");
}
return 0;
}
When the exception is thrown in foo, first the destructor for "x"
should be run
outputting "Running destructor!", then the exception should be caught
in main,
and "Caught exception!" should be output from the handler. This
worked fine
up to -O2, but at -O3 instead the output was:
terminate called after throwing an instance of 'int'
Aborted
The difference between -O2 and -O3 was due to inlining: at -O3 foo was being
inlined into main, and for some reason this was causing the C++ runtime to
terminate the program. To explain why, let me first describe how the GCC
inliner deals with exception handling.
What GCC does
-------------
A "try" clause in C++ contains a certain number of basic blocks (in
the above
example just one basic block containing a call to "foo"). That set of
basic
blocks is one example of an exception handling region in GCC. There are various
types of regions, and these can be nested (just as try clauses can be nested).
As well as regions corresponding to "try" clauses, there are also
"cleanup"
regions. These represent scopes for which some destructor needs to be run when
the scope is left. The function "foo" in the example has a cleanup
region that
represents the fact that the destructor for "x" needs to be run when
the
function is left due to an exception being unwound:
void foo() {
struct X x;
---\
throw 1; | region with a cleanup to be run (the destructor for x)
---/
}
int main() {
try {
---\
foo(); | region corresponding to the "try" statement
---/
} catch (int) { <-- a handler for the above try region
printf("Caught exception!\n");
}
return 0;
}
GCC's exception handling regions correspond in a fairly straightforward way
to
the list of "actions" that the exception unwinder actually sees: for a
cleanup
region there is a cleanup action, for a try region there is one action for each
catch clause. When the "throw" statement is executed, the unwinder
looks up
the call stack and sees:
action: catch "int" [ in function main ] /\ direction of
stack
action: cleanup [ in function foo ] || unwinding
There is additional information in the unwind tables, such as where control
should jump to to run the cleanup (i.e. run the destructor), where to jump
to if the exception matches the catch clause (i.e. if the type of the object
thrown is "int") etc, but this additional information is not important
in what
follows.
When GCC inlines foo into main it simply nests the exception regions of foo
inside those of main:
int main() {
try {
------------ region corresponding to the "try"
statement ---\
struct X x; |
---\ |
throw 1; | region with a cleanup to be run (the destructor for x) |
---/ |
|
------------------------------------------------------------/
} catch (int) { <-- a handler for the above try region
printf("Caught exception!\n");
}
return 0;
}
When the "throw" statement is executed, the unwinder looks up the call
stack
and sees:
action: catch "int" [ in function main ] /\ direction of
stack
action: cleanup [ in function main ] || unwinding
The important point here is that the list of actions seen by the unwinder has
not changed, though the functions they occur in have. That's just as well
because some programs look up the call stack and decide what they are going to
do based on what actions they see. For example, the libstdc++ implementation
of throw first looks up the call stack and if the only actions are cleanups
then it terminates the program without bothering to unwind the exception. If
inlining removed handlers from the action list it could change the program to
one that is terminated by the throw implementation. Another example is (so
I'm
told) the Darwin objective-c runtime which in order to support a dynamic form
of destructor looks up the stack to see what catch clauses are there and does a
bunch of tricky stuff based on what it discovers. Since the GCC inliner never
changes the list of actions seen by the unwinder, inlining doesn't change
the
decisions made by code of this kind.
Let me say it again because it is important: the GCC inliner has the following
fundamental property:
***************************************************************************
* Inlining never changes the list of actions seen by the unwinder when it *
* looks up the call stack. *
***************************************************************************
I am going to argue that the fundamental reason why LLVM has problems with
inlining in the presence of exception handling is that it doesn't follow
this
rule. And that until it does follow this rule exception handling will not work
100% correctly: it will continue to break tricky code that makes decisions by
looking up the call stack to see what catch clauses etc are up there.
What LLVM did
-------------
Why did the example program die with
terminate called after throwing an instance of 'int'
Aborted
if compiled by LLVM with inlining enabled? In short, because the inliner
changed the set of actions on the call stack so that there was only a cleanup
there; the code implementing "throw" looked up the call stack, saw
that there
were only cleanups, and called terminate rather than unwinding the exception.
Here's the LLVM IR for foo:
define void @_Z3foov() noreturn {
entry:
%memtmp = alloca %struct.X, align 8 ; <= Stack object for variable
"x"
%D.2669_1 = call i8* @__cxa_allocate_exception(i64 4) nounwind
%D.2687_2 = bitcast i8* %D.2669_1 to i32*
store i32 1, i32* %D.2687_2, align 4
invoke void @__cxa_throw(i8* %D.2669_1, i8* bitcast
(%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*), void (i8*)* null)
noreturn ; <= Call "throw"
to label %invcont unwind label %rewind
invcont: ; preds = %entry
unreachable
rewind: ; preds = %entry
%exc_ptr = call i8* @llvm.eh.exception()
%filter = call i32 (i8*, i8*, ...)* @llvm.eh.selector(i8* %exc_ptr, i8*
bitcast (i32 (i32, i64, i8*, i8*)* @__gxx_personality_v0 to i8*), i32 0) ; <=
i32 0 means that we are running a cleanup
call void @_ZN1XD2Ev(%struct.X* %memtmp) inlinehint ; <= Run the
destructor
for "x"
call void @_Unwind_Resume(i8* %exc_ptr) noreturn ; <= Keep unwinding
unreachable
}
The GCC cleanup region containing the "throw" becomes an invoke of
"throw" in
LLVM. The fact that this is a cleanup is indicated by the "i32 0" in
the call
to eh.selector. Note that you have to rummage around in the landing pad of the
invoke, i.e. the basic block indicated by the invoke's unwind label, in
order
to find this out.
Symbolically the code for foo can be represented as:
void foo() {
invoke "throw int" ; action: cleanup ---\
|
run destructor <--/ cleanup code here
continue unwinding
}
Here's the LLVM IR for main:
define i32 @main() {
entry:
invoke void @_Z3foov() ; <= Call foo
to label %return unwind label %lpad
return: ; preds = %entry
ret i32 0
lpad: ; preds = %entry
%exc_ptr = tail call i8* @llvm.eh.exception()
%filter = tail call i32 (i8*, i8*, ...)* @llvm.eh.selector(i8* %exc_ptr, i8*
bitcast (i32 (i32, i64, i8*, i8*)* @__gxx_personality_v0 to i8*), i8* bitcast
(%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*)) ; <= @_ZTIi means we
catch type "int"
%typeid = tail call i32 @llvm.eh.typeid.for(i8* bitcast
(%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*))
%0 = icmp eq i32 %filter, %typeid ; <= Was the exception object of type
"int"?
br i1 %0, label %handler, label %rewind
handler: ; preds = %lpad
%D.2679_3 = tail call i8* @__cxa_begin_catch(i8* %exc_ptr) nounwind
%1 = tail call i32 @puts(i8* getelementptr inbounds ([18 x i8]* @.str1, i64
0, i64 0)) ; <= Output "Caught exception!\n"
tail call void @__cxa_end_catch() nounwind
ret i32 0
rewind: ; preds = %lpad
tail call void @_Unwind_Resume(i8* %exc_ptr) noreturn ; <= Keep unwinding
unreachable
}
The GCC try region containing the call to foo becomes an invoke of foo. If you
look in the landing pad lpad you find a call to eh.selector which indicates that
the action is "catch int". It is followed by code to see if the
exception type
matches "int". If so, the handler is run (in block
"handler"); otherwise code
to keep unwinding the exception is run.
Symbolically the code for main can be represented as:
int main() {
invoke foo ; action "catch int" ---\
return 0; |
was it really an "int"? <--/ handler code here
yes: print message, cleanup the exception object, return 0
no: continue unwinding
}
The entries in eh.selector after the personality argument correspond exactly to
the "actions" that the unwinder sees (the code generators create
tables in the
object file that list the actions for each region, i.e. for each invoke; the
tables are read by the unwinder). So when the "throw" statement is
executed,
the unwinder looks up the call stack and sees:
action: catch "int" [ in function main ] /\ direction of
stack
action: cleanup [ in function foo ] || unwinding
So far this is exactly the same as what GCC produced, and indeed if the inliner
is not run then the program works fine.
Here's the LLVM IR after running the inliner:
define i32 @main() {
entry:
%D.2669_1.i = call i8* @__cxa_allocate_exception(i64 4) nounwind
%D.2687_2.i = bitcast i8* %D.2669_1.i to i32*
store i32 1, i32* %D.2687_2.i, align 4
invoke void @__cxa_throw(i8* %D.2669_1.i, i8* bitcast
(%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*), void (i8*)* null)
noreturn ; <= Call throw
to label %invcont unwind label %rewind.i
invcont: ; preds = %entry
unreachable
rewind.i: ; preds = %entry
%exc_ptr.i = call i8* @llvm.eh.exception()
%filter.i = call i32 (i8*, i8*, ...)* @llvm.eh.selector(i8* %exc_ptr.i, i8*
bitcast (i32 (i32, i64, i8*, i8*)* @__gxx_personality_v0 to i8*), i32 0) ; <=
the i32 0 indicates a "cleanup"
%1 = call i32 @puts(i8* getelementptr inbounds ([20 x i8]* @.str, i64 0, i64
0)) nounwind ; <= Output "Running destructor!\n"
invoke void @_Unwind_Resume(i8* %exc_ptr.i) noreturn ; <= Continue
unwinding,
but in fact branch to %lpad because of the invoke
to label %.noexc unwind label %lpad
.noexc: ; preds = %rewind.i
unreachable
_Z3foov.exit: ; No predecessors!
br label %return
return: ; preds = %_Z3foov.exit
ret i32 0
lpad: ; preds = %rewind.i
%exc_ptr = tail call i8* @llvm.eh.exception()
%filter = tail call i32 (i8*, i8*, ...)* @llvm.eh.selector(i8* %exc_ptr, i8*
bitcast (i32 (i32, i64, i8*, i8*)* @__gxx_personality_v0 to i8*), i8* bitcast
(%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*)) ; <= Catch type
"int"
%typeid = tail call i32 @llvm.eh.typeid.for(i8* bitcast
(%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*))
%2 = icmp eq i32 %filter, %typeid ; <= Was the exception object of type
"int"?
br i1 %2, label %handler, label %rewind
handler: ; preds = %lpad
%D.2679_3 = tail call i8* @__cxa_begin_catch(i8* %exc_ptr) nounwind
%3 = tail call i32 @puts(i8* getelementptr inbounds ([18 x i8]* @.str1, i64
0, i64 0)) ; <= Output "Caught exception!\n"
tail call void @__cxa_end_catch() nounwind
ret i32 0
rewind: ; preds = %lpad
tail call void @_Unwind_Resume(i8* %exc_ptr) noreturn ; <= Keep unwinding
unreachable
}
Symbolically this can be represented as:
int main() {
invoke "throw int" ; action: cleanup ---\
|
run destructor <--/ cleanup code here
invoke "continue unwinding"; action "catch int" ---\
return 0; |
was it really an "int"? <--/ handler
code here
yes: print message, cleanup the exception object, return 0
no: continue unwinding
}
The way the LLVM inliner works is (in essence) the following: when inlining
through an invoke
invoke foo_bar ; actions: A, B, C...
it turns all calls it inlines into invokes, and attaches the actions A, B, C...
to them. However if it inlines an invoke it just leaves it alone, and
doesn't
attach A, B, C... to it. This is unlike GCC which nested everything it inlines
inside any exception handling regions containing the call site, which in LLVM
speak would mean that it attaches actions A, B, C... to everything it inlines,
both calls and invokes.
When the "throw" statement is executed, the unwinder looks up the call
stack
and sees:
/\ direction of stack
action: cleanup [ in function main ] || unwinding
So it terminates the program rather than unwinding the exception, because it
only saw cleanups.
In order to preserve the list of actions the unwinder sees, what the inliner
should do is append the "catch int" action to the cleanup action,
giving
int main() {
invoke "throw int" ; actions: cleanup, "catch int"
---\--------------------\
| |
run destructor <--/ cleanup code here
|
invoke "continue unwinding"; action "catch int" ---\
|
return 0; | |
was it really an "int"? <--/ catch
handler here <--/
yes: print message, cleanup the exception object, return 0
no: continue unwinding
}
Another way of saying this is that the eh.selector call
%filter.i = call i32 (i8*, i8*, ...)* @llvm.eh.selector(i8* %exc_ptr.i, i8*
bitcast (i32 (i32, i64, i8*, i8*)* @__gxx_personality_v0 to i8*), i32 0)
should be transformed into the following when inlined:
%filter.i = call i32 (i8*, i8*, ...)* @llvm.eh.selector(i8* %exc_ptr.i, i8*
bitcast (i32 (i32, i64, i8*, i8*)* @__gxx_personality_v0 to i8*), i32 0, i8*
bitcast (%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*))
Then, when the "throw" statement is executed, the unwinder would look
up the
call stack and see:
action: catch "int" [ in function main ] /\ direction of
stack
action: cleanup [ in function main ] || unwinding
i.e. the same set of actions as with GCC, and the same set of actions as when
the inliner isn't run, and all would be well.
A nice additional improvement would be to change the invoke of "continue
unwinding" (aka _Unwind_Resume) into a branch to the catch handler, but
this
is fairly orthogonal to the current discussion so I won't discuss it. GCC
does do this optimization. Except for that, if you append the "catch
int"
to the "cleanup" as described above then GCC and LLVM produce the same
code.
The flaw in the inliner's logic
-------------------------------
Why does the inliner work the way it does? The logic behind it is plausible
but wrong, and goes something like this. An invoke instruction does not let
exceptions unwind through it (instead control branches to the landing pad).
So when inlining it into another function, there is no need to do anything
with it: it is not a throwing instruction. This logic assumes that inlining
does not affect whether an exception is unwound or not. It is true that *if*
an exception is unwound then the code produced by the LLVM inliner will
function correctly. But what if it is not unwound in the first place because
whoever is doing the unwinding looks up the call stack and bases its decision
as to whether to unwind (or potentially even what type of exception it unwinds)
on what actions it sees there? Then the whole LLVM approach falls apart. And
that's exactly what happens in the example program.
What LLVM did next
------------------
My "solution" to the problem of the example aborting was unfortunately
wrong and
has been causing trouble ever since. Dale told me this at the time IIRC, and he
even pointed out what should be done (i.e. the contents of the eh.selector call
for the invoke call site being inlined through should be appended to any calls
to eh.selector inlined through it, see discussion of the example above). But I
didn't get it and instead changed llvm-gcc to output a "catch all"
wherever it
would normally output a cleanup. Then after inlining the unwinder would see the
following up the call stack
/\ direction of stack
action: "catch ..." [ in function main ] || unwinding
and wouldn't terminate the program any more. Of course inlining was still
changing the list of actions seen by the unwinder (which is wrong), but other
problems were introduced too. For example, if a C++ program really only had
cleanups then it wouldn't immediately abort any more when an exception was
thrown, instead it would run all destructors and only abort afterwards. This
may not seem so bad (the C++ standard says it is implementation defined as to
whether destructors are run or not) but in fact there is code in libstdc++ that
depends on the program being aborted without cleanups being run. Also, the
objective-c runtime "knows" all kinds of stuff such as: if there is a
catch
clause then __cxa_begin_catch will be run in the handler; but this wasn't
true
any more. Recent unwinder libraries also "know" things of this kind
and have
implemented optimizations based on it (which break due to the "catch
all").
The list goes on and on. I should have listened to Dale.
When LLVM does now
------------------
Currently the LLVM inliner is still doing the wrong thing. However the
workaround of turning cleanups into catch-alls has been partially abandoned.
Instead of turning cleanups into catch-alls, llvm-gcc outputs a cleanup as
it should. Rather, the code generators try hard to correct the problems
introduced by inlining, shifting catch actions from one invoke to another
(and occasionally adding in a catch-all here and there). This is not always
possible but the result is something that works in practice. However the
fundamental property of inlining not altering the list of actions seen by the
unwinder does not hold.
What LLVM should do
-------------------
When inlining through an invoke callsite, the list of actions for the invoke
should be appended to everything inlined, which can be achieved by appending
it to every eh.selector call that is inlined. If this was done then inlining
would no longer change the set of actions up the call stack, the workarounds
in the code generator would go away, and LLVM behaviour would be more correct,
and in fact would be identical to GCC's.
But there's a problem here: how to find the actions for the invoke? They
are
listed in the eh.selector call which should be in the invoke's landing pad.
But the optimizers can move it out of the landing pad, and indeed in theory
eh.selector calls could occur far away from the landing pad (though this in
not true in practice for LLVM IR produced by llvm-gcc and clang).
The obvious solution is to simply attach the list of actions directly to the
invoke instruction, which is the content of my recent exception handling
proposal. Then the inliner can just grab them and append them to those of
any invokes it inlines through the call site.
Bill's recent exception handling proposal has rules which (if I understand
them right) ensure that you can always find the actions for an invoke without
any trouble (the actions would be listed in a dispatch instruction that is
not allowed to be moved out of the landing pad). Thus with this proposal too
there would also be no problem in teaching the inliner to do the right thing.
This presumably would also simplify the proposal since the dispatch instruction
would no longer need to specially know about the "catch all" for the
language
since the code generators would no longer need to know about catch-all (they
currently do to work around the inliner's faulty logic, see above).
However it also possible to improve things without any IR changes at all (but
not make things perfect). The inliner can always *try* to find the actions
associated with an invoke, even if it is not guaranteed to find the call to
eh.selector. If it can't find the eh.selector then too bad (in this case it
could act as if they were an empty list, which is of course wrong). If it can
find the actions, it would then append them to everything it inlines, i.e. to
every eh.selector call it inlines. Most of the time it will find the actions -
this is clear because the code generator actually aborts with an assert failure
if it can't find them itself! So clearly the code generators always finds
them
in practice - the inliner just needs to try at least as hard as the code
generators. The result is that the inliner will do the right thing for all
code that the code generator currently doesn't abort on, and the codegen
logic
that tries to fix up lists of actions by shifting them around can be removed.
In short, the situation would be strictly improved, but of course it would still
just be a workaround while waiting for an improved exception handling design.
Duncan.
John McCall
2010-Dec-07 00:01 UTC
[LLVMdev] Inlining and exception handling in LLVM and GCC
On Dec 6, 2010, at 1:58 PM, Duncan Sands wrote:> The poor interaction between exception handling and inlining in LLVM is one of > the main motivations for the new exception handling models proposed recently. > Here I give my analysis of the origin of the problem in the hope of clarifying > the situation.Your analysis coincides with the analysis I made when implementing EH in clang. I decided, however, that in the short term, given the brokenness of the IR, I would rather ignore the known bad consequences of emitting cleanups as catch-alls than deal with the uncertainties of whether codegen could consistently clean up after our broken inliner. I haven't yet regretted this choice.> Why does the inliner work the way it does? The logic behind it is plausible > but wrong, and goes something like this. An invoke instruction does not let > exceptions unwind through it (instead control branches to the landing pad). > So when inlining it into another function, there is no need to do anything > with it: it is not a throwing instruction. This logic assumes that inlining > does not affect whether an exception is unwound or not. It is true that *if* > an exception is unwound then the code produced by the LLVM inliner will > function correctly. But what if it is not unwound in the first place because > whoever is doing the unwinding looks up the call stack and bases its decision > as to whether to unwind (or potentially even what type of exception it unwinds) > on what actions it sees there? Then the whole LLVM approach falls apart. And > that's exactly what happens in the example program.Right. Another way of thinking about this is that the LLVM inliner generally assumes that the call itself is not semantically relevant. Unfortunately, while that's often true in practice, there are actually two major semantic effects tied to call boundaries. The first is that function return implicitly releases stack-allocated memory; the pathological case here is a recursive function that calls a helper with huge stack variables, where inlining the helper makes max recursion depth plummet. Currently the inliner makes some effort to mitigate this impact, but mostly by sharing allocas between different inlined functions. The second is that inlining changes the behavior of anything that wants to manually walk the call stack, and while most stack walkers don't rely on any frame-specific semantics, the personality functions called by libunwind do. And unfortunately, the inliner currently makes no effort at all to compensate for breaking these call-specific semantics, even though they're arguably way more important in practice than the frame-size issue. John. -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20101206/68fa6e8d/attachment.html>
Duncan Sands
2010-Dec-07 11:38 UTC
[LLVMdev] Inlining and exception handling in LLVM and GCC
Hi John, On 07/12/10 01:01, John McCall wrote:> On Dec 6, 2010, at 1:58 PM, Duncan Sands wrote: >> The poor interaction between exception handling and inlining in LLVM is one of >> the main motivations for the new exception handling models proposed recently. >> Here I give my analysis of the origin of the problem in the hope of clarifying >> the situation. > > Your analysis coincides with the analysis I made when implementing EH > in clang. I decided, however, that in the short term, given the brokenness > of the IR, I would rather ignore the known bad consequences of emitting > cleanups as catch-alls than deal with the uncertainties of whether codegen > could consistently clean up after our broken inliner. I haven't yet regretted > this choice.if I changed the inliner so it does its best to find the eh.selector for an invoke, and propagates the information onto the things it inlines, would clang make use of it (i.e. output cleanups rather than turning them into catch-alls)? Ciao, Duncan.
Renato Golin
2010-Dec-07 11:54 UTC
[LLVMdev] Inlining and exception handling in LLVM and GCC
Hi Duncan, Amazing post, thank you very much! Now I have a clear mental image of the whole problem and can see where you were coming from to propose such a change. On 6 December 2010 21:58, Duncan Sands <baldrick at free.fr> wrote:> Bill's recent exception handling proposal has rules which (if I understand > them right) ensure that you can always find the actions for an invoke without > any trouble (the actions would be listed in a dispatch instruction that is > not allowed to be moved out of the landing pad). Thus with this proposal too > there would also be no problem in teaching the inliner to do the right thing.If I got it right, you're saying that both proposals are similar in that they both fix the inlining problem and both specify uniquely the landing pads for every invoke. So, in view of their equivalence, I think Bill's proposal is better for two reasons: 1. It eases the future elimination of invoke, or at least, the treatment of current instruction-level exception (as in Java) in a cleaner way. 2. It reinforces the idea of having one personality function for each EH table (ie. per function), especially when inlining code from different paradigms (if that's possible). However, your proposal is better in two other accounts: 1. If we do want to support EH tables with multiple personalities in the future. 2. It's less invasive and closer to the problem it meant to fix in the first place. So it'll be faster and easier to do that way. It's more of a design decision, IMO. -- cheers, --renato http://systemcall.org/ Reclaim your digital rights, eliminate DRM, learn more at http://www.defectivebydesign.org/what_is_drm
Duncan Sands
2010-Dec-07 20:20 UTC
[LLVMdev] Inlining and exception handling in LLVM and GCC
Hi Renato,>> Bill's recent exception handling proposal has rules which (if I understand >> them right) ensure that you can always find the actions for an invoke without >> any trouble (the actions would be listed in a dispatch instruction that is >> not allowed to be moved out of the landing pad). Thus with this proposal too >> there would also be no problem in teaching the inliner to do the right thing. > > If I got it right, you're saying that both proposals are similar in > that they both fix the inlining problem and both specify uniquely the > landing pads for every invoke.yes, they both make the inlining problem fixable because it is always possible to find the catch info associated with an invoke (or basic block). In fact any proposal that has this property would do as far as fixing inlining goes. In the case of Bill's proposal this property holds because of special rules about what you are allowed to do with landing pads: you are not allowed to move a dispatch instruction out of a landing pad etc. These rules could actually be applied to eh.selector without any IR changes providing much the same effect without a new instruction. However the reason for not doing this is that it then makes some optimizations impossible, for example when you inline a call to _Unwind_Resume through an invoke you would like to turn it into a branch to the landing pad, but this would result in eh.selector calls that fail the rules. It's not clear to me if the dispatch instruction has this problem too, I need to ask Bill about it. In fact Bill's proposal mostly seems to be about replacing the current explicit sequence of comparisons that llvm-gcc uses to find which handler to run (I guess everyone who has looked at LLVM IR with exception handling in it has seen all the calls to eh.typeid.for, the comparisons and the branching that I'm talking about here) with a more compact representation that is easier to analyse. As such, from my point of view it doesn't really have much to do with the inlining issue, which is not to say it doesn't have merit on other grounds. An interesting point is that Bill's dispatch instruction is quite analogous to GCC's gimple_eh_dispatch statement. This statement represents the transfer of control to the appropriate catch handler for an exception region, and is at a fairly high level of abstraction. It takes an exception handling try region as an argument (it can also have an "allowed exceptions" region as an argument, but there's no added value in discussing that). Since the list of associated catches is stored in the region info, there is no need for it to explicitly list tries and where to branch to, while Bill is obliged to put the info that GCC stores in the region directly into his dispatch instruction. Once GCC has finished running the inliner it runs the lower_eh_dispatch pass, which removes all gimple_eh_dispatch statements by turning them into explicit control flow: a sequence of comparisons and branches (exactly like the code llvm-gcc/clang produce right now, comparing the selector value with the result of eh.typeid.for calls). Since my proposal doesn't change this aspect of LLVM, you can consider that before this pass GCC's representation is like Bill's and after it is like in my proposal.> So, in view of their equivalence, I think Bill's proposal is better > for two reasons: > > 1. It eases the future elimination of invoke, or at least, the > treatment of current instruction-level exception (as in Java) in a > cleaner way.I don't see what is cleaner about it, except that it is overall at a higher level of abstraction (see above).> 2. It reinforces the idea of having one personality function for > each EH table (ie. per function), especially when inlining code from > different paradigms (if that's possible).According to Bill's proposal each dispatch instruction can specify a different personality function, so it's just the same as my proposal in this respect.> > However, your proposal is better in two other accounts: > > 1. If we do want to support EH tables with multiple personalities in > the future.As mentioned above, Bill's proposal also supports multiple personalities per function.> 2. It's less invasive and closer to the problem it meant to fix in > the first place. So it'll be faster and easier to do that way. > > > It's more of a design decision, IMO.Exactly, and this brings up the question of what criteria should be used to decide which proposal is best. Presumably, all else being equal, whichever one makes it easiest to optimize the IR. I didn't yet think about the advantages and disadvantages of each proposal in this respect. Ciao, Duncan.