Extending gcc's exception delivery path (was: question about arithmetic exceptions)

Godmar Back gback@cs.utah.edu
Mon Nov 29 09:35:00 GMT 1999


Okay, this has become a rather long email because I switch topic
midway; nevertheless, it would be nice if the people who have experience
with exceptions would read it and comment on it.

Let me first reply to Andrew's comments:

> 
> Sure, that would work.  No disagreement.  But does the problem justify
> such a large reorganization of the compiler?
> 

I believe the division hack is just one problem, and admittedly a minor
one at that.  The other problems are more important IMO.

> 
> Surely it would be better to override some libgcc functions in
> language libraries.  That way, the default versions in gcc could stay
> as they are and not bother other languages that aren't affected by
> this problem.

Yes, this is what I would advocate.

> 
> > For instance, I am convinced that the exception delivery path in Java
> > could be sped up substantially if exceptions didn't have to be delivered
> > out-of-band via thread-specific data.
> 
> Surely if this is true of Java it is also true of any other language
> that uses exceptions.
> 

Yes, and every language may have its own twists to it.
If I remember it right, doesn't C++ allow you to throw any data type,
not just "objects" like Java.  C++'s requirements/options for a faster 
delivery path are probably different than Java's --- hence the need for
a language-specific path.  For instance, in C++ you could deliver
large exception objects in multiple registers or the like (this is just
a wild guess.)


> > if languages could override/customize libgcc, gij didn't have to
> > resort to the gnu.gcj.runtime.MethodInvocation hack --- you could
> > just handle the delivery of exceptions back to the interpreter
> > specially.
> 
> I don't quite understand what you mean by this.  Surely _Jv_Throw
> could be changed to do this without touching libgcc.
> 

It's not clear to me how that would work.  Changing _Jv_Throw wouldn't
affect much, IMO.  What you'd have to change would be the matcher function,
but even so it's not clear to me how it would work.

What I was thinking of is something similar to what I do in Kaffe.  
(That's actually something I want to suggest to the gcc community later, 
once it's completely done, but why not filling you in earlier to get
some feedback):

What I'm suggesting is a way to extend the exception delivery path
in libgcc2.c.  I'm talking about range based exception handling only at
this point.
When an exception is thrown, __throw unwinds the stack.  This process
requires looking up information about each frame and processing it.
Such information may include instructions about how to restore registers,
where to find exception handlers, etc.  If __throw doesn't find the
information about a given stack frame, it will bail via terminate ---
w/o having the ability to resume. (*)

The problem comes in if there's stack frames for which no accurate 
exception handling information exists.  This is true for just-in-time 
compiled kaffe code, but I think it's also true for gij.  What gij 
currently does is to nest itself in an all-catching MethodInvocation 
frame.  If an exception is thrown, the gij frame is skipped, the exception 
is caught, and gij handles it.  You're paying a run-time cost in the
common case which is something you shouldn't.  (**)

In Kaffe, I may have to catch an exception thrown by gcj in a kaffe/jit
frame, or I may have to catch an exception thrown by the jit in gcj
code, or I may have to restore registers from a jit frame to a gcj
handler.  One way of doing that is to construct dwarf2 information and
register it via register_frame.

This may have been an option, but it's not how I did it, for three reasons:
First, dwarf2's full complexity is not needed.  For instance, because of
kaffe's jit simplicity, it will always eagerly save callee-saved registers,
making them easy to find on a stack frame.

Second, the mechanism to register and unregister frame information is 
more suited for registering/unregistering the information for complete 
shared modules.  It is not well-suited for a just-in-time compiling system 
where methods are translated piece by piece as they're invoked.

Third, kaffe delivers the exception object directly in a register 
(not out-of-band as gcj does), and it doesn't wrap synchronized methods 
in try/finally.
Hence, I needed to ability to invoke kaffe exception handlers directly,
something that would have been very complicated to code in dwarf's 
little language. (****)
The fact that synchronized methods are done differently requires me
to unlock the objects while unwinding the frame.

In any event, to make a long story short, this is the change I had to make
to libgcc.c: in next_stack_level:

The original code reads like this:

    static void *
    next_stack_level (void *pc, frame_state *udata, frame_state *caller_udata)
    {
      caller_udata = __frame_state_for (pc, caller_udata);
      if (! caller_udata)
	return 0;
    ...

(frame_state_for extracts the information for the stack frame at "pc"
into "caller_udata")

I changed it to invoke a second function if no information is found:
(***)

    static void *
    next_stack_level (void *pc, frame_state *udata, frame_state *caller_udata)
    {
      frame_state *c_udata;
      c_udata = __frame_state_for (pc, caller_udata);

      if (! c_udata && (*__external_frame_state_for))
	c_udata = (*__external_frame_state_for)(pc, udata->cfa, caller_udata);

      if (! (caller_udata = c_udata))
	return 0;
    ...


In essence, what you see here is a way to hook into gcc's delivery
system in a way that would allow non-registered frames to provide the
information otherwise obtained from DWARF2 information.  It also
allows them to deliver the exception directly to such frames, if
desired.  This is the reason I'm passing the CFA "udata->cfa".
This, I believe, is not only useful to kaffe, but to *any* dynamic
code generation or interpretation system that needs to interface to
the exception handling path of precompiled code.

There's two arguments I could see against that.

First, one could argue that there's an additional cost in the case
where the mechanism is not used.  (i.e., the additional test whether
__external_frame_state_for is there.)  I would claim that the gains
of such a mechanism would outweigh this cost, and that the cost is
minimal compared to the work that's done in __frame_state_for.

Second, one could argue that this is opening a "backdoor" that may prove
a legacy once people have come to rely on it.  This is true, but frankly
I think that's no worse than all the other "built-in" functions gcc
already exports in one way or another, or the specifics of asm("")
and how they change from version to version.  This is not desirable
nor optimal, clearly, but I think having the ability to extend the
delivery system (especially in such a localized way) would make up
for it.

Of course, my fallback solution would be carry my own eh portion
of libgcc2.c around.  For this fallback solution to be more maintainable,
it would be nice if libgcc2 were split up.  (Isn't anybody else bothered
by how ugly the current setup is, I mean aesthetically?)

I would like it if people commented on this approach/suggestion.

	- Godmar


(*) Somebody tell me if this statement isn't true.  (Is it possible to
rethrow unhandled exception may be when overriding terminate?)

(**) Let's not argue about whether optimizing interpreting code is 
good or bad or urgent, ok?

(***)
This is a hack, an actual solution would provide methods such as

    frame_state_for_func_t e;

    set_external_unwind_hook(frame_state_for_func_t t)
    {
	    e = t;
    }

and would read:

    if (! c_data && e != 0)
	    c_udata = e(...);

(****)
Btw, I strongly believe this approach is superior for Java: once libgcj
gets to the stage where it can compile and run SpecJVM98, you'll
notice how poorly gcj will perform on exception-intensive tests such
as _228_jack.  In my current prototype, it's outperformed even by kaffe's jit.



More information about the Java mailing list