[Gc] workaround for C++ exceptions problem

skaller skaller at users.sourceforge.net
Thu Jan 26 00:14:37 PST 2006


On Thu, 2006-01-26 at 00:36 -0500, Filip Pizlo wrote:
> On Jan 26, 2006, at 12:11 AM, skaller wrote:

> Oh, well.  const or no const, you obviously figured out what I was  
> after. :-)

Yup.

> How would the machine stack be faster?  When you throw the exception,  
> you can't put it on the top of the stack, because the top of the  
> stack will be blown away and reused by destructors.  So where would  
> you put the exception?

In a register whilst you're decrementing the stack.

In the first case, you CAN push the exception onto the stack
when you execute a destructor, and pop it off afterwards,
pop the stack, push it onto the stack again to execute the
next destructor.

Such accesses are free of cost since the cache already
contains the region of the stack, or it will on the
very next instruction, which is a subroutine call to
the destructor. SMLY on returning the subroutine pops
the return address off the stack -- so again the stack
is in the cache .. so the chance of a cache miss is extremely
low. Operations on the cache take zero time (in effect).

Also, it wastes only one machine word per 'in flight'
exception, which is half the space required for a list.

One register is required for scratch to cover the 
reduction of the stack pointer (usually an increment
of SP reduces the stack .. so I say reduce the stack
pointer rather than decrement it .. :)

Something like:

	pop EAX           // save exception
	add SP,frame_size // unwind the stack
	push EAX          // restore exception

Basically stack unwinding is just a single instruction,
with my layout it takes 3 instructions. This is probably
enough except when checking to see if there is a matching
handler (and there is some fiddling on exit from
the handler ..)

In fact, when a handler is called .. the current exception
is right there on the stack like a subroutine call argument
should be.

I suppose I could fill in the details, but I guess 
you get the idea. Although this does execute a lot more
instructions track the current exception .. it's probably
faster than using the heap simply because stack protocol
storage access is the fastest possible access (other than
registers of course :)

This is purely an artefact of caching or other properties
of memory which favour accesses which are close together.

> I don't see any reasonable way of using the machine stack for  
> exceptions.

Hope you can now! The point is that the top exception
has to be somewhere special .. but the rest don't.

You may ask, well what happens if we pop the next from
top right off the stack during unwinding???

The answer is: this is the very case that isn't allowed.
If that were to happen -- that's precisely when you have
a double fault and call terminate().

Therefore, unwinding process can use the stack for
all but the top (innermost, most recent, current) exception,
and since unwinding is only a single instruction, a single
register is all that is required as scratch between destructor
calls.

There ARE some reasons NOT to do this. One is that it 
probably interferes with tail call optimisation.

Optimisation of tail calls is essential for performance ..
the separate heap doesn't get in the way -- the exception
on the stack does.

So in the end you're right .. the separate linked list is
probably the fastest .. not in the 'usual' case but for
the reason I said: it is easier to manage, eg, it
will work more transparently with optimisations like
tail call optimisation.

> Right, that's exactly what you do.  Users have to throw exceptions  
> using the GC_THROW function.  Every use of:
> 
> throw e;
> 
> must be replaced with:
> 
> GC_THROW(e);

What about exceptions thrown from inside the standard library?

> >
> > Also minor problem .. if the object contains pointers
> > into itself .. a bitwise copy will not preserve that.
> 
> Yeah, but why would you care about those pointers?  They would point  
> at an object that the GC doesn't know about, so the GC will ignore  
> them, which is exactly the right thing to do.

Good point!

> >
> > If the object has virtual bases such pointers always
> > exist or can be constructed, even if the user didn't create them.
> > If they're offsets, you'll get a different result to full
> > pointers .. the pointer will point to the real exception.
> >
> > This is messy .. :)
> 
> But why would those pointers cause trouble?

Because thinking about it requires reasoning about C++
which tends to twist my brain inside out :)

In other words .. I don't know .. can you say why
they *wouldn't* cause trouble? You can probably show
many examples I could dream up can't cause a problem --
can you prove it in general?

OTOH if you don't copy the exception .. you don't need
to worry about it :)

> Right, but that is not a big problem.  Look at my reply to Hans.   
> I've pretty much convinced myself that the following will work:
> 
> 1) Replace all uses of 'throw' with GC_THROW(), where GC_THROW() does  
> something like:
> 
> template< typename T >
> void GC_THROW(const T &exn) {
>     try {
>        throw exn;
>     } catch (T &exn) {
>        GC_add_exception_roots(&exn,&exn+1);
>        throw;
>     }
> }
> 
> 2) Make sure that all exceptions call the following method in their  
> destructor:
> 
> MyException::~MyException() {
>     GC_remove_exception_roots_whose_range_includes(this);
> }

Yup, looks good to me also.

-- 
John Skaller <skaller at users dot sf dot net>
Felix, successor to C++: http://felix.sf.net



More information about the Gc mailing list