I'm trying to find some quantitative cost (even if only a vague one) to
throwing and catching an exception. I had one person assert that the cost
might be "tens of thousands of instructions", which is difficult for me to
believe. Is that plausible?
--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]
Exceptions are expensive.
The compiler must maintain a stack of catch statements
and match the expression object against them.
The cost depends on how many unmatching traps are inbetween.
My debugger does not allow me to stop at the handler
so I tried to do it by hand;
I was unable to arrive at the handler
by stepping the machine instructions from the throw point
because it took too long,
although that effect may be attributed to stack unwinding
and object destruction as well.
I think those tens of thousands of instructions include the cleanup code.
Exceptions should not be used for situations
that may happen during normal execution flow.
An example of such abuse is std::locale::locale(char const []).
Yes, I know exceptions are the recommended way
to handle run-time errors in constructors;
this approach seems very unfortunate to me.
Notice, however, that std::basic_fstream does not follow this pattern.
Moreover, it is hard to write code
that does not leak resources or corrupt data when exceptions appear.
This is especially true with respect to developers
that know how to program in C.
Exception-proof C++ code does not resemble C code at all.
It is a very different beast.
You should make sure you all know how to write such code.
The team of the latest project I was on did not
- remember that joke where BG goes to heaven?
This is the philosophy of rapid coding:
compile, sell, collect, run (to Uruguay, for example).
OTOH, there are advantages to using exceptions:
the exception object can contain a detailed diagnostic
about the error condition;
passing such an object all the way from the handler to the exceptional condition and back
is cumbersome, unintuitive and it makes your code difficult to maintain.
If you throw such objects and you can extract valuable information from them,
exceptions are the way to go.
But be warned that C++ with exceptions != C++ + exceptions.
Cheers,
Chris
> I'm trying to find some quantitative cost (even if only a vague one) to
> throwing and catching an exception. I had one person assert that the cost
> might be "tens of thousands of instructions", which is difficult for me to
> believe. Is that plausible?
The question is, what is your point of comparison? A thrown exception
can easily casue "tens of thousands of instructions" to execute
between the throw point and the final catch point, but a large portion
of that could be because of destructors being called during the stack
unwinding. If you were going to save some state and do a lot of early
returns from your functions, then you would hit that same cost.
What may be useful for you is to know some of the steps of the
exception throwing machinery.
Before you even throw an exception, a try block may set up some
bookkeeping information. Not all implementations do this, as it slows
down the "normal" cases. The implementations that have fast normal
execution will likely have slower exceptional execution though.
After that, the runtime has to match the thrown exception with a catch
statement. This will involve RTTI-like calls to account for exception
heirarchies. This could be as expensive as one "dynamic_cast" per
catch handler.
Somewhere along the way, the exception is copied. If your exception
objects are big, this may cause a problem.
The stack then starts unwinding, calling all destructors along the
way. Performance is likely to suffer here because the memory manager
is getting a workout, and you are probably page faulting like mad
because you are going through infrequently executed code (at least for
a large app).
The main place where an end user can save some performance is in the
first few matching steps. If you are manually return and matching
errors to handlers is easy, then you may not end up going through all
the matching steps. However, this will increase your code size, since
now each function will effectively have two sets of exception handling
code (the compiler's generated version, and yours), so this may end up
slowing things down in the end.
What you should definitely do is profile your code to see what the
performance difference is. Just be wary that running exceptional code
in a loop will hide a lot of the performance issues that would exist
in the wild, particularly those related to code locality.
> The conventional wisdom is that exceptions in C++ are expensive
well, that certainly used to be the case when they were first introduced
into the language but I don't think it's worth worrying about now.
> I'm trying to find some quantitative cost (even if only a vague one) to
> throwing and catching an exception.
I wouldn't bother. Measure the application as a whole to find where the
bottlenecks are. They are almost always in unexpected places that you
wouldn't have second-guessed. I would be very suprised if any of those
bottlenecks turned out to be due to exceptions. If they are than those
particular ones identified by the profiler would be the ones to get rid
of, along with a comment saying that the use of exceptions was changed in
that area as a result of performance profiling results.
--
Andrew Marlow http://www.andrewpetermarlow.co.uk
There is an emerald here the size of a plover's egg!
Don't top-post http://www.catb.org/~esr/jargon/html/T/top-post.html
Plain text mails only, please http://www.expita.com/nomime.html
The only way to be sure is to measure. However, see this thread:
http://groups.google.com/group/alt.comp.lang.learn.c-c++/browse_frm/thread/1a5e7fef082b5472/
Deep into the thread, some timing results are presented. You may draw
your own conclusions from them.
--
Marcus Kwok
Replace 'invalid' with 'net' to reply
On Jun 1, 9:53 pm, Roy Smith <r...@panix.com> wrote:
> The conventional wisdom is that exceptions in C++ are expensive, but I've
> never seen anything that say *how* expensive they are. I come from a
> background using languages where exceptions are not particularly expensive,
> and thus tend to use them in paces where perhaps I shouldn't (at least if
> you believe the grief my co-workers give me).
>
> I'm trying to find some quantitative cost (even if only a vague one) to
> throwing and catching an exception. I had one person assert that the cost
> might be "tens of thousands of instructions", which is difficult for me to
> believe. Is that plausible?
Not sure of the number, though exception objects in C++ are handled
differently than normal objects.
So, there is a definite overhead. Exception stack unwinding sort of
runs conjugate to the normal function stack winding
and lot of temporary data is created and destroyed, if an exception is
thrown. Assuming there are a large hierarchy of exception objects and
an exception occurs which involves a complex set of throw (and catch)
blocks, the overhead will be significant in terms of memory and
speed.
-----------
http://oops-concepts.blogspot.com
There is an official paper about that and much more, called the
Technical Report ISO/IEC 18015:2006 on C++ Performance. The latest
public draft is available here
http://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf
You will find section 5.4 very interesting.
> I'm trying to find some quantitative cost (even if only a vague one) to
> throwing and catching an exception. I had one person assert that the cost
> might be "tens of thousands of instructions", which is difficult for me to
> believe. Is that plausible?
That order of magnitude seems an exaggeration to me. Anyway the issue is
more complex than that, as you can't just compare non-EH code with EH
code by measuring the time spent throwing an exception. For example,
even if the cost of launching an exception may be high (and I'm not
saying it necessarily is), it's always something that should occur only
in *exceptional* cases, while you may be saving a lot of return-value
checks that would have been performed in *all* cases. So the application
might actually end up being *faster* with EH in the non-exceptional
cases (which is where you really want your code to be fast).
HTH,
Ganesh
To specifically answer the question on how expensive they are, that
depends on the compiler. Most compiliers implement in such a way that
entering a try block is cheap, and throwing is expensive. To compare
to Java, one would have to use finally blocks to reclaim every
resource that was acquired from the start of the try. This includes
memory. This is a feature of C++ RAII BTW, and occurs when you exit
the scope of ANY block, try or not. Reclaiming resources as part of
the applciation control flow (as opposed to deferring to a background
GC) can make it look like exceptions are expensive. What is really
expensive is freeing all the resources acquired.
In any case, dont do this
try{//bad way to do a for loop
int i=0;
while(true){
do_something();
if i++>100 throw 1;
}
}
}catch(...){}
Do do this
class Sumlessthan100{
unsigned x,y;
public:
Sumlessthan100(unsigned a, unsigned b):x(a),y(b){
if(!(x+y<100)||x>99 ||y>99){
std::ostringstream s;
s<<__FILE__<<":"<<__LINE__<<
"Sumlessthan100("<<x<<","<<y<<") sum must be less than
100";
throw std::logic_error(s.str());
}
}
Sumlessthan100(Sumlessthan100 const&r):x(r.x),y(r.y){
//does not throw, since r is guaranteed to be valid
}
unsigned getX()const;
unsigned getY()const;
void setX(unsigned newx){
Sumlessthan100 tmp(newx,y);
x=newx;
}
};
and with a small army of classes like this then you do
int processrequest(){//some very high level function
try{
unsigned myx=getInput(); //may throw
unsigned myy=getInputagain();//may throw
Sumlessthan100 cleaninput(myx,myy);//may throw
//at this point we enter the "no-fail zone"
processsideeffect1(cleaninput); //does not throw
processsideeffect2(cleaninput); //does not throw
}catch(std::exception const&e){
cout<<e.what();
return -1;
}catch(...){
cout<<"Unknown error";
return -1;
}
return 0;
}
Now imagine a program with dozens of inputs and hundres of validation
-- doing it without exceptions would be very painful. Exception may be
excessive overhead for very small programs, but scaling up without
them is masochistic.
Lance
> Exceptions are expensive. The compiler must maintain a stack of
> catch statements and match the expression object against them.
No, that's just one approach. See
http://groups.google.com/group/comp.lang.c++.moderated/browse_frm/thread/b5288eef67ec6c55
also, http://gcc.gnu.org/ml/java/1999-q1/msg00016.html gives a good
overview of the main strategies along with a novel alternative. The
range table approach uses the return addresses on the program stack
and program counter to do all the dynamic bookkeeping.
> The cost depends on how many unmatching traps are inbetween. My
> debugger does not allow me to stop at the handler so I tried to do
> it by hand; I was unable to arrive at the handler by stepping the
> machine instructions from the throw point because it took too long,
> although that effect may be attributed to stack unwinding and object
> destruction as well. I think those tens of thousands of
> instructions include the cleanup code.
>
> Exceptions should not be used for situations that may happen during
> normal execution flow. An example of such abuse is
> std::locale::locale(char const []). Yes, I know exceptions are the
> recommended way to handle run-time errors in constructors; this
> approach seems very unfortunate to me.
It's actually very important in enabling simple class invariants.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
> The only way to be sure is to measure. However, see this thread:
>
> http://groups.google.com/group/alt.comp.lang.learn.c-c++/browse_frm/t...
>
> Deep into the thread, some timing results are presented. You may draw
> your own conclusions from them.
Those are based on a rather old version of GCC that uses a different
ABI and exception handling mechanism than the current one.
--
They're not really expensive.
The fact that exceptions gives sense to error flow means that
actually, one path may be optimized.
It is usually the path where no error occurs which is optimized.
On "good" implementations, exceptions are simply zero-cost unless
they're thrown.
Which makes them way more efficient than return values and error codes
when errors are not so usual.
> I come from a
> background using languages where exceptions are not particularly expensive,
> and thus tend to use them in paces where perhaps I shouldn't (at least if
> you believe the grief my co-workers give me).
>
> I'm trying to find some quantitative cost (even if only a vague one) to
> throwing and catching an exception. I had one person assert that the cost
> might be "tens of thousands of instructions", which is difficult for me to
> believe. Is that plausible?
Throwing an exception usually requires allocating memory on the heap
for that exception. I do not know if implementations can avoid copies
from the stack to the heap.
Catching requires the type to be identified with RTTI.
The dispatch is just handled with tables, and is quite efficient,
while it generates a lot of code size.
void f()
{
A arr[N];
// ...
}
A::A()
{
// ...
if (something) f(); // recursive call
else return;
// ...
}
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
> David Abrahams wrote:
>> also, http://gcc.gnu.org/ml/java/1999-q1/msg00016.html gives a good
>> overview of the main strategies along with a novel alternative. The
>> range table approach uses the return addresses on the program stack
>> and program counter to do all the dynamic bookkeeping.
>>
> The return address and PC value are obviously insufficient during
array
> construction. You have to store and _update_ the number of elements so far
> created:
No, the code is already storing and updating that value. It's either
in a register or on the stack somewhere, or it wouldn't know which
element to construct next.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
Well, it seems I have to back up a bit.
It seems
exceptions provide a natural implementation for transactional programming.
Suppose you have a sequence of actions to perform
where each individual action can be undone;
if any action fails, everything must be undone.
This requirement appears e.g. when you create entries in the system
registry;
it is implemented in an interpreted way by ATL IRegistrar interface.
(Things usually get scripted
if they are difficult to get right in ordinary executable code,
e.g. when some reflection is needed on what is happening and why.)
It seems
that the implementation of this requirement using exceptions is
straightforward:
- specify your actions as automatic objects, not as function calls;
- let each object's constructor perform the update, throwing upon failure;
- let each object's destructor undo the update
if it is being destroyed because of an exception
(which can be detected using std::uncaught_exception(void)).
You can do the same thing without exceptions
using either polymorphism or template code;
however, the simplicity of the throwing solution is appealing.
Rolling back
>:-b
>
> Throwing an exception usually requires allocating memory on the heap
> for that exception. I do not know if implementations can avoid copies
> from the stack to the heap.
They can. In fact, it's possible to keep the exception on the stack
in some implementations that don't move the stack pointer until
unwinding is complete.
> Catching requires the type to be identified with RTTI.
Sorta.
> The dispatch is just handled with tables, and is quite efficient,
> while it generates a lot of code size.
Maybe.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> Catching requires the type to be identified with RTTI.
This is a very common misconception about exceptions.
In fact, catch blocks can be resolved at compile time,
and RTTI _might_ be used but is not required at all.
Observe this program to print "Base" (the compile-time
type of the thrown object) whereas if RTTI were used it
would print "Derived" (the run-time type of the thrown
object):
- - -
struct Base
{
virtual ~Base()
{
}
};
struct Derived : public Base
{
};
int main()
{
Base *p = new Derived;
try
{
throw *p;
}
catch( const Derived & )
{
cout << "Derived" << endl;
}
catch( const Base & )
{
cout << "Base" << endl;
}
catch( ... )
{
cout << "<unknown>" << endl;
}
delete p;
return 0;
}
- - -
Cheers!
- Risto -
--
P.S. EH imposes certain run-time overhead, you know ;)
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
And the "well-known place" cannot be a register? Why not?
If the compiler creates a loop to initialize an array, where is the
loop counter? When generating the exception handler, the compiler can
use this knowledge.
::
:: P.S. EH imposes certain run-time overhead, you know ;)
No, I don't. :-)
Bo Persson
It also removes flow arcs from not having to constantly check return
codes, so more optimizations become possible.
Bo Persson
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> :: P.S. EH imposes certain run-time overhead, you know ;)
>
> No, I don't. :-)
>
So you do :)
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> David Abrahams wrote:
>> > The return address and PC value are obviously insufficient during
>> > array construction. You have to store and _update_ the number of elements
>> > so far created:
>> >
>> No, the code is already storing and updating that value. It's either
>> in a register or on the stack somewhere, or it wouldn't know which
>> element to construct next.
>>
> 1. Yes, the code updates this value.
> 2. No, having EH enabled the compiler has to store and update it in a
> well-known place rather than use fast "any-register" allocation.
I have no idea what you mean by "any-register" allocation. Ignoring
red herrings like register frames that can be restored just like
registers, it's always possible to tell what register something will
be stored in by looking at the code. Look, I know you don't believe
me, but it's trivially true. If the ctor of the array element can
throw an exception, there is a real function call involved somewhere.
The compiler needs to generate code that can restore and access the
counter after a function call, so that it can construct the next
element and increment the counter again.
> P.S. EH imposes certain run-time overhead, you know ;)
Of course it depends what you're measuring against. If your baseline
is, "errors are ignored," then yes, it must impose an overhead in a
few cases. If your baseline is, "errors are handled by some other
means," then EH need not impose an overhead and can often be an
efficiency win.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ warning: long post]
Here's some data you might find interesting. These are results from
(part of) Joe Orost's bench++.
Result set 1: Microsoft C++ x64 edition
Test Name: E000001 Class Name: Exception
CPU Time: 5.12 microseconds plus or minus 0.256
Wall/CPU: 1.01 ratio. Iteration Count: 6553600
Test Description:
Time to raise and handle an exception
Exception defined locally and handled locally
Test Name: E000002 Class Name: Exception
CPU Time: 6.14 microseconds plus or minus 0.307
Wall/CPU: 1.01 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is in a method of a class
that has its own exception handler
Test Name: E000003 Class Name: Exception
CPU Time: 3.91 microseconds plus or minus 0.195
Wall/CPU: 1.01 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 3 deep in procedure calls
Test Name: E000004 Class Name: Exception
CPU Time: 3.84 microseconds plus or minus 0.192
Wall/CPU: 1.00 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Compare to E000003
Test Name: E000006 Class Name: Exception
CPU Time: 4.32 microseconds plus or minus 0.216
Wall/CPU: 1.02 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Exception thrown is declared. Compare to E000004
Test Name: E000007 Class Name: Exception
CPU Time: 32.9 microseconds plus or minus 1.65
Wall/CPU: 1.00 ratio. Iteration Count: 409600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Exception caught and re-thrown at each level. Compare to E000004
Test Name: E000008 Class Name: Exception
CPU Time: 35.2 nanoseconds plus or minus 1.76
Wall/CPU: 1.00 ratio. Iteration Count: 419430400
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Here exception is replaced with setjmp/longjmp. Compare to E000004
Result set 2: g++ 3.4 (mingW)
Test Name: E000001 Class Name: Exception
CPU Time: 832 nanoseconds plus or minus 41.6
Wall/CPU: 1.00 ratio. Iteration Count: 26214400
Test Description:
Time to raise and handle an exception
Exception defined locally and handled locally
Test Name: E000002 Class Name: Exception
CPU Time: 1.26 microseconds plus or minus 0.0632
Wall/CPU: 1.00 ratio. Iteration Count: 26214400
Test Description:
Exception raise and handle timing measurement
when exception is in a method of a class
that has its own exception handler
Test Name: E000003 Class Name: Exception
CPU Time: 827 nanoseconds plus or minus 41.4
Wall/CPU: 1.000 ratio. Iteration Count: 26214400
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 3 deep in procedure calls
Test Name: E000004 Class Name: Exception
CPU Time: 821 nanoseconds plus or minus 41.1
Wall/CPU: 1.00 ratio. Iteration Count: 26214400
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Compare to E000003
Test Name: E000006 Class Name: Exception
CPU Time: 820 nanoseconds plus or minus 41.0
Wall/CPU: 1.00 ratio. Iteration Count: 26214400
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Exception thrown is declared. Compare to E000004
Test Name: E000007 Class Name: Exception
CPU Time: 3.36 microseconds plus or minus 0.168
Wall/CPU: 1.00 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Exception caught and re-thrown at each level. Compare to E000004
Test Name: E000008 Class Name: Exception
CPU Time: 54.5 nanoseconds plus or minus 2.73
Wall/CPU: 1.000 ratio. Iteration Count: 419430400
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Here exception is replaced with setjmp/longjmp. Compare to E000004
Result set 3: Microsoft VC++ 7.1 with SSE instructions enabled:
Test Name: E000001 Class Name: Exception
CPU Time: 5.36 microseconds plus or minus 0.268
Wall/CPU: 1.00 ratio. Iteration Count: 6553600
Test Description:
Time to raise and handle an exception
Exception defined locally and handled locally
Test Name: E000002 Class Name: Exception
CPU Time: 5.95 microseconds plus or minus 0.297
Wall/CPU: 1.00 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is in a method of a class
that has its own exception handler
Test Name: E000003 Class Name: Exception
CPU Time: 5.32 microseconds plus or minus 0.266
Wall/CPU: 1.00 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 3 deep in procedure calls
Test Name: E000004 Class Name: Exception
CPU Time: 5.34 microseconds plus or minus 0.267
Wall/CPU: 1.00 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Compare to E000003
Test Name: E000006 Class Name: Exception
CPU Time: 5.35 microseconds plus or minus 0.267
Wall/CPU: 1.00 ratio. Iteration Count: 6553600
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Exception thrown is declared. Compare to E000004
Test Name: E000007 Class Name: Exception
CPU Time: 28.8 microseconds plus or minus 1.44
Wall/CPU: 1.00 ratio. Iteration Count: 1638400
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Exception caught and re-thrown at each level. Compare to E000004
Test Name: E000008 Class Name: Exception
CPU Time: 42.2 nanoseconds plus or minus 2.11
Wall/CPU: 1.00 ratio. Iteration Count: 419430400
Test Description:
Exception raise and handle timing measurement
when exception is raised nested 4 deep in procedure calls
Here exception is replaced with setjmp/longjmp. Compare to E000004
These were all run on the same machine. Of course, others will have
faster or slower machines than mine, but that's likely to affect all the
results by approximately a constant factor. This is probably semi-
representative of what to expect with a reasonably current desktop
machine of the x86 variety. My guess is that the ratio between
throw/catch and setjmp/longjmp will remain pretty large on most other
machines as well.
--
Later,
Jerry.
The universe is a figment of its own imagination.
{ quoted clc++m banner removed -mod }
Cant you just write two similar small programs one with EH and one
without and just disassamble them?
even with assembly view in most of the editors...
> Ignoring
> red herrings like register frames that can be restored just like
> registers, it's always possible to tell what register something will
> be stored in by looking at the code. Look, I know you don't believe
> me, but it's trivially true.
>
We don't talk about religion, don't we? ;)
My point is: The return address and PC value are insufficient during
array
construction. Look:
void f() { A a1, a2 /* ... */ aN; }
void g() { A a[N]; }
In the f()'s case every aI object is initialized at its own PC
value.
But in the g()'s case every a[I] object is initialized at the same
code
location.
> > P.S. EH imposes certain run-time overhead, you know ;)
>
> Of course it depends what you're measuring against. If your baseline
> is, "errors are ignored," then yes, it must impose an overhead in a
> few cases.
>
Errors aren't ignored (hint: RAII).
But the source code must not be modified, you're only allowed to
turn the EH
compiler switch On/Off.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
the problem is in the detail. If you do not use EH then you have to
provide some other form of error handling else the programs are not
comparable.
Nice measurements. I'd love to also see the cost of successful try/catch
vs. if/else in a couple of cases (e.g. no nesting and 4-level deep nesting).
An article published in DDJ (I think in 2004 or 2005) ran measurements
of successful try/catch blocks and showed that there is performance
degradation compared to the if/else case even for zero-cost
implementations. The authors suggested that might be due to the compiler
generating more conservative code.
I tried to dig the article but couldn't find it. Does anyone remember
that article?
Andrei
--
That's one measurement. Another meaningful measurement is to see how
much more it costs just to have exception handling turned on, without
any modification to the code. That would give the overhead of the
exception handling mechanism per se.
--
Seungbeom Kim
On Wed, 6 Jun 2007, Risto Lankinen wrote:
> Observe this program to print "Base" (the compile-time
> type of the thrown object) whereas if RTTI were used it
> would print "Derived" (the run-time type of the thrown
> object):
>
> - - -
>
> int main()
> {
> Base *p = new Derived;
>
> try
> {
> throw *p;
> }
> catch( const Derived & )
> {
> cout << "Derived" << endl;
> }
> catch( const Base & )
> {
> cout << "Base" << endl;
> }
> catch( ... )
> {
> cout << "<unknown>" << endl;
> }
>
> delete p;
>
> return 0;
> }
Catching and throwing are different things. Catching is done based on the
run-time type. Throwing is done based on compile-time type. In particular,
if you modify your program as such:
catch( const Base &e )
{
cout << typeid(e).name() << endl;
}
the printed name will identify Base, not Derived. The thrown object has a
dynamic type of Base.
The reason is simple: it is a *copy* of the throw-expression that is
thrown, not the throw-expression itself. Not the object you allocated on
the heap is thrown, but a copy of it. Go ahead, give your object a copy
constructor. It will be called. Specifically, Base's copy constructor is
called, because that's how copying works. It's just like calling a
function with a by-value argument of type Base and passing *p.
Oh, by the way, you're also slicing the object.
Sebastian Redl
Yes. When an exception is thrown, there is an object to be constructed, and
most exceptions contain a string object which may contain another
allocation. As the stack is unwound, unrelated destructors of automatic
objects are called. When a try block is encountered, it has to do a
dynamic_cast-like type comparison, which can get very complicated,
especially if the RTL includes error-checking for access rights to base
classes. It can get pretty ugly. But on modern machines, even embedded
systems, we're generally talking millisecond-level stuff, so in many apps
it's really no big deal.
If you're worried about overhead, but still want to use exceptions, I
recommend seeing if you can at least turn off the checking of exception
specs. In gcc, it's -fno-enforce-eh-specs. That'll get rid of a lot of
checking that really just assists debugging, not actual operation.
--
Ciao, Paul D. DeRocco
Paul mailto:pder...@ix.netcom.com
> David Abrahams wrote:
>
>> Ignoring
>> red herrings like register frames that can be restored just like
>> registers, it's always possible to tell what register something will
>> be stored in by looking at the code. Look, I know you don't believe
>> me, but it's trivially true.
>>
> We don't talk about religion, don't we? ;)
No
> My point is: The return address and PC value are insufficient during
> array
> construction. Look:
>
> void f() { A a1, a2 /* ... */ aN; }
> void g() { A a[N]; }
>
> In the f()'s case every aI object is initialized at its own PC
> value.
> But in the g()'s case every a[I] object is initialized at the same
> code
> location.
Yes, but, as I've said repeatedly and won't say again, the counter
that tracks which element has been initialized is either in a known
register or in the stack frame. Either way, it's retrievable by the
EH mechanism.
>> > P.S. EH imposes certain run-time overhead, you know ;)
>>
>> Of course it depends what you're measuring against. If your baseline
>> is, "errors are ignored," then yes, it must impose an overhead in a
>> few cases.
>>
> Errors aren't ignored (hint: RAII).
I must be too dense for your hints because they lead me nowhere.
> But the source code must not be modified, you're only allowed to
> turn the EH
> compiler switch On/Off.
In that case you are measuring against "no error handling." If your
program was written to handle out-of-memory using exceptions and you
turn off EH, then when your program runs out of memory it enters the
land of undefined behavior.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> Jerry Coffin wrote:
>
>> Here's some data you might find interesting. These are results from
>> (part of) Joe Orost's bench++.
>
> Nice measurements. I'd love to also see the cost of successful try/catch
> vs. if/else in a couple of cases (e.g. no nesting and 4-level deep nesting).
>
> An article published in DDJ (I think in 2004 or 2005) ran measurements
> of successful try/catch blocks and showed that there is performance
> degradation compared to the if/else case even for zero-cost
> implementations. The authors suggested that might be due to the compiler
> generating more conservative code.
The "zero-cost" refers to the cost of EH when no exceptions are
thrown, not the cost of actually unwinding. Almost nobody bothers to
optimize that.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> I'm trying to find some quantitative cost (even if only a vague one) to
> throwing and catching an exception. I had one person assert that the cost
> might be "tens of thousands of instructions", which is difficult for me to
> believe. Is that plausible?
I find that using exceptions make programming much easier. It IMHO
makes code much more readable, and it allows for one to really use the
one function, one responsibility principle to better effect (as
opposed to deep nested if else statements, that make code complex,
unreadable, and lead to the possibility of resource leaks easier).
That said, the expensiveness of exceptions rely very much on the
architecture. We are in the process of wrapping the Posix tasking
primitives, and wrote code where a couple of task rally for resources
amongst them, whilst acquiring/releasing mutexes in a tight loop. On
the x86 architecture the fact that we were using exceptions seemed to
made no or very little difference in the overall execution time. We
were never throwing, but the code had the ability to throw. The
outcome, however were guarded by a try/catch. OTOH, on our 32 bit
embedded ARM processor, just the mere fact that possibility to throw
existed made it crawl, which really surprised me to the point where I
consider removing it. I wondered about this, and figured that perhaps
exceptions influenced the size of the stack in such a way that perhaps
the stack itself was not cacheable during context switch, but maybe
I'm talking bollocks. I could not think of any other reason. In both
cases we used the GNU as compiler.
What would the reason be for this significant difference?
Werner
--
It was about the successful paths. The article analyzed the cost of the
successful paths in try/catch code, and concluded that even in the
zero-cost implementations, the generated code suffered of a performance
loss compared to the if/else version.
Andrei
--
--
> > Errors aren't ignored (hint: RAII).
>
> I must be too dense for your hints because they lead me nowhere.
>
You don't have to write something like:
if (err) {
fclose(f);
return;
}
// OK: do something useful
// ...
fclose(f);
return;
I.e. RAII helps you to deal with errors no matter EH is enabled or not.
> > But the source code must not be modified, you're only allowed to
> > turn the EH compiler switch On/Off.
> >
> In that case you are measuring against "no error handling."
>
Suppose you have the following call chain:
f()->g1()->g2() ... ->gN()->h()
In this case h() can throw exceptions and f() contains the corresponding
catch() blocks. The point is that the middle layer g1(), g2() ... gN() can
use RAII no matter EH is enabled or not. I.e. your can compile
g_functions.cpp with EH switch turned to On and Off and compare the object
code...
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
> David Abrahams wrote:
>> > void f() { A a1, a2 /* ... */ aN; }
>> > void g() { A a[N]; }
>> >
>> > In the f()'s case every aI object is initialized at its own PC
>> > value.
>> > But in the g()'s case every a[I] object is initialized at the
same
>> > code location.
>> >
>> Yes, but, as I've said repeatedly and won't say again, the counter
>> that tracks which element has been initialized is either in a known
>> register or in the stack frame. Either way, it's retrievable by the
>> EH mechanism.
>>
> OK, I see I should provide some more information:
> 1. Your statement that the range table and PC value is sufficient to
> unwind
> the stack is false. In the case of array construction you also need the
> number of elements so far created.
You also need any amount of other data that may be stored on the stack
variables, explicitly or implicitly, including the chain of return
addresses. That data is always there, so I didn't think it worth
mentioning. The loop counter is an implicit automatic variable. It
might even be implemented as an explicit automatic variable in the
runtime library.
> 2. Your (old good) statament that EH can be build with true zero
> runtime overhead is false.
As I said in my previous message and have consistently tried to point
out, it depends with respect to what you are measuring.
> At least in the case of array construction compiler must take an
> additional effort to make the number of elements so far created
> accessible for the EH unwinder. This effort kills certain local
> optimizations.
Then name just one, and show how it is killed.
>> > Errors aren't ignored (hint: RAII).
>>
>> I must be too dense for your hints because they lead me nowhere.
>>
> You don't have to write something like:
>
> if (err) {
> fclose(f);
> return;
> }
>
> // OK: do something useful
> // ...
>
> fclose(f);
> return;
>
> I.e. RAII helps you to deal with errors no matter EH is enabled or not.
I don't see the relevance here. RAII doesn't get you out of checking
err when exceptions are turned off.
>> > But the source code must not be modified, you're only allowed to
>> > turn the EH compiler switch On/Off.
>> >
>> In that case you are measuring against "no error handling."
>>
> Suppose you have the following call chain:
>
> f()->g1()->g2() ... ->gN()->h()
>
> In this case h() can throw exceptions and f() contains the corresponding
> catch() blocks. The point is that the middle layer g1(), g2() ... gN() can
> use RAII no matter EH is enabled or not.
The middle layers cannot propagate an error code if the error
condition was communicated via EH before you turned the switch off.
So that's "no error handling."
> I.e. your can compile g_functions.cpp with EH switch turned to On
> and Off and compare the object code...
And what does that tell you? Those g functions don't do the same
thing they did before you turned off the EH switch.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> > I find that using exceptions make programming much easier.
> >
> IMHO this statement isn't quite correct. I'd say "using RAII makes
> programming much easier":
This is probably OT, but I [do] find my statement to be the case.
I have found that typically (most of the time) one can simplify a
complex function by breaking it up in many functions that perform one
thing only. These simple functions only check for the positive case
where after returning to the master function that in turn invokes the
next simple function. If any of the simple functions fail, they throw.
The throwing code 90% (hopefully 100%) of the time never gets executed
at all.
Usually the master, before invoking any of the functions verify mutual
pre-conditions. This programming style almost does not require RAII,
but typically if the functions share state that is dependent on
resources, then RAII is probably a necessity. This also IME prevent
duplication of code as result of complex ifs. I'll admit that
exception handling could be difficult without RAII, if not in some
cases impossible.
Without exception handling (for those rare exceptions), one would have
to write much more complex code for the rare exceptions (Ifs, ifs and
more ifs). We typically only throw if its worth noting. We have a
style of soft assertions that check for conditions and throw instead
of asserting. Often in the field the program may not assert (apart
from the fact that it won't for releases). We catch throws on the
highest level only, because it's truly exceptional. Each task (or
thread) is associated with a global logging interfaces at the highest
level and logs the "real exceptions". For me it is hard to imagine
propagating every possible error up to the highest level. Yes, people
do, they use exit( STATUS_ERROR ), or something to that effect to
propagate. Using return types for this and iffing for each case, is
nightmarish IMO.
RAII in combination with exceptions, apart from exceptions overhead
(which I only now started to notice) for me leads to simpler code
Most often our functions consist of small functions that don't have
much more than one if statement. Here I may be wrong, but apart from
the overhead that EH has on the stack, making use of this style of
programming also leads to a staggered stack, instead of a deep stack
caused by nesting.
A simple case where exceptions for us worked really well, is
verification of input data:
We have a thread that receives data. Upon receipt of the data, the
thread pushes the next callback on the mailbox (that will receive the
next data), therefore calling itself recursively whilst simultaneously
always unwinding the stack with each recursion. The data is then
verified, and upon verification success it is sent to observers. Any
failure causes an exception, which propagates to the highest level,
gets logged where after the next callback is popped of the queue
(receiving more data). The receiver of the data doesn't care about
errors at all. The task of verifying the data is totally isolated, and
no ifs exist, simplifying the code drastically.
As a last, the reason I mentioned deep nested ifs making resource
leaks easier, is that one tends to miss easily (during maintenance)
that resources were allocated. While RAII is ideal for this, negating
nested ifs are also beneficial (hence my statement).
Regards,
Werner
> David Abrahams wrote:
>> on Thu Jun 07 2007, "Andrei Alexandrescu (See Website For Email)"
>> <SeeWebsiteForEmail-AT-erdani.org> wrote:
>>> An article published in DDJ (I think in 2004 or 2005) ran measurements
>>> of successful try/catch blocks and showed that there is performance
>>> degradation compared to the if/else case even for zero-cost
>>> implementations. The authors suggested that might be due to the compiler
>>> generating more conservative code.
>>
>> The "zero-cost" refers to the cost of EH when no exceptions are
>> thrown, not the cost of actually unwinding. Almost nobody bothers to
>> optimize that.
>
> It was about the successful paths. The article analyzed the cost of the
> successful paths in try/catch code, and concluded that even in the
> zero-cost implementations, the generated code suffered of a performance
> loss compared to the if/else version.
I'd like to see that article.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
But that number is available, and can be retrieved using the PC
value as a guide. This issue has come up over and over, and has
been explained again and again, and somehow it's never enough.
It's really very simple. As the array is being constructed, the
code is somehow tracking how many elements have been done. It
could be using a loop and a register counter, or a memory counter,
or the entire construction can be unrolled into a series of code.
It doesn't matter. The unwind code for that PC is written to know
how to construction code is keeping track, and can therefore do the
destruction of the right number of elements.
> At least in the case of array construction compiler must
> take an additional effort to make the number of elements so far created
> accessible for the EH unwinder. This effort kills certain local
> optimizations.
There is no additional effort. The EH code for each PC is wriiten
to use whatever facility the construction code is using.
Perhaps an example could make you understand? Suppose we have the
following code:
extern void f(); // potential thrower
struct a { a() { f(); } ~a() { } };
int main() { a as[5]; }
The compiler decides to unroll the construction loop, so it looks
like this:
a *p = stackalloc(5 * sizeof(a));
L0: new(&p[0]) a;
L1: new(&p[1]) a;
L2: new(&p[2]) a;
L3: new(&p[3]) a;
L4: new(&p[4]) a;
Notice that there's no counter at all, because the loop is unrolled
and the number of elements is known at compile time. Each of the calls
to a's constructor can throw, so there has to be a handler for each.
The handlers look like this (and fall through):
Handler_When_L4_Throws: p[3].~a();
Handler_When_L3_Throws: p[2].~a();
Handler_When_L2_Throws: p[1].~a();
Handler_When_L1_Throws: p[0].~a();
Handler_When_L0_Throws: // keep unwinding...
The unwind code uses the PC, namely one of L0-L4, to select the place
to go to do the cleanup. As you should see, no counter needs to be kept.
[ ... ]
> Nice measurements. I'd love to also see the cost of successful try/catch
> vs. if/else in a couple of cases (e.g. no nesting and 4-level deep nesting).
That would be an interesting addition. I'm not sure if I'll have time,
but with a bit of luck I might be able to write a bit of code for a test
sometime soon...
> An article published in DDJ (I think in 2004 or 2005) ran measurements
> of successful try/catch blocks and showed that there is performance
> degradation compared to the if/else case even for zero-cost
> implementations. The authors suggested that might be due to the compiler
> generating more conservative code.
>
> I tried to dig the article but couldn't find it. Does anyone remember
> that article?
Not right off. I looked a little bit at the DDJ web site, but they seem
to only want you to look at articles from the current issue. Somewhere
around here I believe I have a DVD of older articles, but first I'll
have to find the DVD...
--
Later,
Jerry.
The universe is a figment of its own imagination.
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> This issue has come up over and over, and has
> been explained again and again, and somehow it's never enough.
>
> It's really very simple.
>
Not that fast, please. Unrolled loops (you've presented) isn't the
case I'm
talking about.
Please make some comments on my previous example (where N is too big
to be
unrolled):
void f()
{
A arr[N];
// ...
}
A::A()
{
// ...
if (something) f(); // recursive call
else return;
// ...
}
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
> > OK, I see I should provide some more information:
> > 1. Your statement that the range table and PC value is sufficient
to
> > unwind the stack is false. In the case of array construction you also
need
> > the number of elements so far created.
> >
> You also need any amount of other data that may be stored on the stack
> variables, explicitly or implicitly, including the chain of return
> addresses.
>
I'm talking about a _conceptual_ point. It's not a "detail".
> > At least in the case of array construction compiler must take an
> > additional effort to make the number of elements so far created
> > accessible for the EH unwinder. This effort kills certain local
> > optimizations.
>
> Then name just one, and show how it is killed.
>
More constraints mean less optimizations.
I.e. you need to build very long and complex range tables to be able
to
fetch the number of elements so far created from any possible location (good
optimization means any possible location, even XORing two values in one
register!). You'll never find such tables in practice.
> > I.e. RAII helps you to deal with errors no matter EH is enabled or not.
>
> I don't see the relevance here. RAII doesn't get you out of checking
> err when exceptions are turned off.
>
Even with enabled EH you check for local errors. I.e. EH doesn't
mean "no
error checking".
> > In this case h() can throw exceptions and f() contains the corresponding
> > catch() blocks. The point is that the middle layer g1(), g2() ... gN()
can
> > use RAII no matter EH is enabled or not.
>
> The middle layers cannot propagate an error code if the error
> condition was communicated via EH before you turned the switch off.
> So that's "no error handling."
>
You should say "no error handling communicated via EH". So what?
> > I.e. your can compile g_functions.cpp with EH switch turned to On
> > and Off and compare the object code...
>
> And what does that tell you? Those g functions don't do the same
> thing they did before you turned off the EH switch.
>
Yes, they do. They were designed to be exception-neutral.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
Speaking absolutely, yes it can.
> Please make some comments on my previous example
OK, here it is:
extern volatile bool condition;
exetrn voltaile bool die;
extern volatile int s;
struct A { A(); ~A() { s = 0; } };
void f() { A arr[N]; /* ... */ }
A::A() { if (condition) f(); if (die) throw 0; }
Without unrolling, there will indeed need to be a counter
keeping track of constructed elements. So let's look at
how we might compile f, with A::A inlined:
f:
A *p = stackalloc(N * sizeof(A));
int *cc = stackalloc(sizeof(int)); // allocated to a local variable
for (*cc = 0; *cc < N; ++*cc) {
if (condition) f();
if (die) throw 0;
}
Here, cc sits in the stack frame of f. When an exception is thrown, the
handling mechanism unwinds stack frames, and eventually gets back to the
frame in which we were constructing the array. In this frame, p and cc
are both available, and the handler just does
while (*cc-- > 0) (p + *cc)->~A();
Notice that this sequence of code may be encountered multiple times as we
unwind recursive calls to f. That's fine, because the p and cc compiled
into the handler called are all offsets into the stack frame.
Alternatively, the code might registers instead of memory:
f:
register A *p = stackalloc(N * sizeof(A));
register int cc;
for (cc = 0; cc < N; ++cc) {
if (condition) Label: f();
if (die) throw 0;
}
Now, when the construction code calls f recursively, p and cc are in
registers which the recursive call will need to reuse. Therefore, they
must be saved, either before the call or at the beginning of f. That is,
f might be
f: // callee saves
push preg;
push ccreg;
// then as before
or else it might be
// ...
if (condition) {
// caller saves
push preg;
push ccreg;
Label: f();
pop ccreg;
pop preg;
}
In the first case, as the generic handler unwinds an f stack frame,
it pops off the two saved values from the stack and puts them back
into registers. This is a callee-saves situation, and the compiler
will have generated handler information for f to tell the unwinder
which registers to pop as it unwinds an f frame.
In the second case, the particular handler for Label can do the
register pops:
Handler_for_Label: pop ccreg; pop preg;
followed by the destructor loop:
while (cc-- > 0) (p + cc)->~A();
just like before.
The handler code specific to Label is generated at the same time as the
rest of the code, so for any situation it knows where to look to find
the saved counters. And saving the counters isn't just for the sake of
the exception handler; since the arrays are being constructed recursively,
the counters must be saved somewhere in the stack frame.
If you still cannot accept my argument, perhaps you can show a counter
example?
> David Abrahams wrote:
>> >> Yes, but, as I've said repeatedly and won't say again, the counter
>> >> that tracks which element has been initialized is either in a known
>> >> register or in the stack frame. Either way, it's retrievable by the
>> >> EH mechanism.
>> >>
> I'm tying to emphasize a _conceptual_ point: PC value together with
> range
> table is insufficient! But without array construction these two do well,
> though.
No, there's nothing magical about array construction. It's easy to
see that because I can manually write code that has exactly the same
semantics (and thus the same EH issues) and yet uses no array types.
>> > OK, I see I should provide some more information: 1. Your
>> > statement that the range table and PC value is sufficient
>> >
>> > to unwind the stack is false. In the case of array construction
>> > you also need the number of elements so far created.
>>
>> You also need any amount of other data that may be stored on the stack
>> variables, explicitly or implicitly, including the chain of return
>> addresses.
>>
> I'm talking about a _conceptual_ point. It's not a "detail".
It's also not "valid."
>> > At least in the case of array construction compiler must take an
>> > additional effort to make the number of elements so far created
>> > accessible for the EH unwinder. This effort kills certain local
>> > optimizations.
>>
>> Then name just one, and show how it is killed.
>>
> More constraints mean less optimizations. I.e. you need to build
> very long and complex range tables to be able to fetch the number of
> elements so far created from any possible location (good
> optimization means any possible location, even XORing two values in
> one register!). You'll never find such tables in practice.
Irrelevant. Can you show an example of a local optimization that is
impossible with range table EH, and yet allows the same error recovery
to be implemented by other means, or can't you?
>> > I.e. RAII helps you to deal with errors no matter EH is enabled
>> > or not.
>>
>> I don't see the relevance here. RAII doesn't get you out of checking
>> err when exceptions are turned off.
>>
> Even with enabled EH you check for local errors. I.e. EH doesn't
> mean "no error checking".
Of course not. It means one layer of checking instead of N layers
where N is the depth of the call stack.
>> > In this case h() can throw exceptions and f() contains the
>> > corresponding catch() blocks. The point is that the middle layer
>> > g1(), g2() ... gN() can use RAII no matter EH is enabled or not.
>>
>> The middle layers cannot propagate an error code if the error
>> condition was communicated via EH before you turned the switch off.
>> So that's "no error handling."
>
> You should say "no error handling communicated via EH".
I said exactly what I meant and I stand by it.
>> > I.e. your can compile g_functions.cpp with EH switch turned to On
>> > and Off and compare the object code...
>>
>> And what does that tell you? Those g functions don't do the same
>> thing they did before you turned off the EH switch.
>
> Yes, they do. They were designed to be exception-neutral.
No they don't. With EH on they respond correctly to error conditions
detected in h(). With EH off they do not. That's a semantic
difference.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> Yes, they do. They were designed to be exception-neutral.
>
And error-eating.
I think his point was that unless you provide another mechanism for
propagating errors, it is not a fair comparison.
As a simple test(probably too simple) I tried the following:
#include <time.h>
#include <stdio.h>
#include <tchar.h>
static int value=0;
int test4(){
if(value>10000)
return -1;
else
value++;
return 0;
}
int test3(){
value++;
return test4();
}
int test2(){
value++;
return test3();
}
int test1(){
value++;
return test2();
}
void test4a(){
if(value>10000)
throw -1;
else
value++;
}
void test3a(){
value++;
test4a();
}
void test2a(){
value++;
test3a();
}
void test1a(){
value++;
test2a();
}
int _tmain(int argc, _TCHAR* argv[])// OK the compiler is MS C++ (2005)
{
clock_t start=clock();
for(int i=0;i<1000000;i++)
{
for(int k=0;k<1000;k++)
{
int result = test1();
if(result)
{
printf("Failed");
return result;
}
}
value=0;
}
// try // Alternative using Exceptions
// {
// for(int i=0;i<1000000;i++)
// {
// for(int k=0;k<1000;k++)
// test1a();
// value=0;
// }
// }catch(int x){
// printf("Failed");
// return x;
//
// }
//
printf("time= %d",clock()-start);
}
// END
Using a common compiler and disabling Exceptions, this produce the
output: time= 6687
Switching the loops run and enabling EH, produced: time= 6140
Enabling exceptions has no effect on the time of the version that does
not use exceptions. But, strangely, disabling EH more than doubles the
time of the version that uses Exceptions.
Also, calling test2A or test2 instead of test1a/test1, which does fewer
increaments, takes longer:6577 and 6859 respectively. And calling test3a
instead takes even longer,7499, while test3 drops the time a bit to 6513.
If the exception/error is not nested(calling test4a and test4)
exceptions take 4999 and error code take 4499. So there are cases even in
this simple test were Error codes are faster.
But there does seem to be some weirdness to my test.
Otis
> Here's some data you might find interesting. These are results from
> (part of) Joe Orost's bench++.
>
> Result set 1: Microsoft C++ x64 edition [ what's this? ]
> [...]
> Result set 2: g++ 3.4 (mingW)
> [...]
> Result set 3: Microsoft VC++ 7.1 with SSE instructions enabled:
> [...]
>
> These were all run on the same machine. Of course, others will have
> faster or slower machines than mine, but that's likely to affect all the
> results by approximately a constant factor. This is probably semi-
> representative of what to expect with a reasonably current desktop
> machine of the x86 variety. My guess is that the ratio between
> throw/catch and setjmp/longjmp will remain pretty large on most other
> machines as well.
Testing GCC 4.2.0, preferably on linux, would probably be more
interesting than 3.4.0 on Windows.
It may also be interesting to test the Intel compiler, since they're
the ones who came up with the famous Itanium ABI for exception
handling.
--
You can argue theory all day. Why don't you just do some real world
testing.
First of all, if you use any exceptions at all in the program, even a
single one, you have to compile the program with options that make you
pay the biggest price for using exceptions, setting up the stack for
every function to be exception aware. So even if you use just one, you
might as well use them everywhere because you already paid the biggest
price of all.
Second, I have done lots of performance testing on different compilers
and there is virtually no significant difference between throwing an
exception of type int, and returning an int and checking the value.
To be fair when performance testing, you have to use the same type
"error" object, so if you like returning ints to check errors, you
have to throw int exceptions if you are doing performance comparison.
Also, remember, you have to do "if (return_code == failed)" type junk
at every function call if you are using error return values instead of
exceptions (otherwise you aren't doing any error handling which is an
unfair test and bad programming). You don't have to do that when
throwing exceptions. You only need one catch clause no matter how deep
the call stack is.
So, the deeper your call stack, the more "ifs" you have to put to
check the return codes of the functions in the stack. The exception
method doesn't have this liability and the deeper your stack, the more
efficient the exception method is.
Probably the reason people think throwing exceptions is slow, is they
are throwing std::string or even more complex exception objects. Sure,
if you have to construct a complex exception object it's going to be
slower but that's not a fair test. If you are going to compare
returning an int and throwing an exception, you have to also use the
same atom of error information.
Why you should keep exception throws to a minimum isn't for
performance. It's because exceptions should only be used to signal a
function failed to perform its job, and because of this, has put the
entire call stack at risk. If this function failed, it's probably
going to cause its caller to fail also, and so forth up the chain
until someone is able to handle that failure. There is no reason to
use exceptions unless you need to return up multiple levels of stack.
You should certainly never throw and catch within the same function.
void f()
{
g();
A arr[N];
g();
}
A::A()
{
g();
if (something) f(); // recursive call
else return;
g();
}
-----------------------------------8<-----------------------------------
using the following options:
1. Full optimization, EH is On.
2. Full optimization, EH is Off.
And compare the assembly code of f() function?
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
> Alternatively, the code might registers instead of memory:
>
> f:
> register A *p = stackalloc(N * sizeof(A));
> register int cc;
> for (cc = 0; cc < N; ++cc) {
> if (condition) Label: f();
> if (die) throw 0;
> }
>
> Now, when the construction code calls f recursively, p and cc are in
> registers which the recursive call will need to reuse. Therefore, they
> must be saved, either before the call or at the beginning of f.
>
This statement isn't quite correct. You should say "they must be
saved
somewhere in the f()'s call chain" if the chain modifies R1 value at all.
Look, if cc lives in R1 and f() calls g() then R1 can be saved in
g().
Moreover, if f() is really complicated and the full possible call
chain is
f()->g()->h(), then R1 can be saved in f(), g(), h() or not saved at all!
The following real case is almost unlimited call chain where some
functions
are extern "C" (which are opaque to the EH runtime except for their return
address and number of locally allocated bytes)...
Do you see how this "plain simple" array construction becomes a real
pain
to the compiler architect? The solution is to always save R1 in the caller.
I.e. having EH enabled you must "pessimize" this code.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
Do you agree? (yes/no)
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
That's fine. If the code is generated using caller-saves,
then the compiler builds an unwind handler for each location
where a function which can throw is called. When the unwinder
pops the stack, it finds the handler for the return location
on the stack and the handler specific to that location knows
which registers have been saved and pops them off.
> The following real case is almost unlimited call chain where some
> functions are extern "C" (which are opaque to the EH runtime except
> for their return address and number of locally allocated bytes)...
Extern "C" is just linkage - such code is regular C++ and deals with
exceptions like any other C++ code. If you're talking about plain C
code and trying to pass exceptions through such functions, that's
obviously not in the scope of the standard. If an implementation
chooses to allow such a thing, it's going to depend on the ABI, and
if that doesn't reliably specify how to unwind and restore registers
then there may need to be an extra caller saves step that would not
otherwise be needed.
> You can argue theory all day. Why don't you just do some real world
> testing.
Well, I have (long ago). The fact is that EH mechanisms can differ
quite a lot and the quality of integration with optimizers can differ
a lot too. But anyway, Sergey is (or at least he started out by)
making claims that certain overheads are unavoidable, which means the
argument _is_ about theory. I know that in at least some (maybe even
in all) real-world implementations, the EH implementation is not quite
optimal.
> First of all, if you use any exceptions at all in the program, even
> a single one, you have to compile the program with options that make
> you pay the biggest price for using exceptions, setting up the stack
> for every function to be exception aware. So even if you use just
> one, you might as well use them everywhere because you already paid
> the biggest price of all.
No such "setting up" is required in a range-table-based
implementation.
> Second, I have done lots of performance testing on different
> compilers and there is virtually no significant difference between
> throwing an exception of type int, and returning an int and checking
> the value.
That's really interesting. I'm guessing you've not tested too many
range-table-based implementations, because the ones I've looked at
make you pay quite a bit for even a little unwinding (in return for a
more efficient no-exception path).
> To be fair when performance testing, you have to use the same type
> "error" object, so if you like returning ints to check errors, you
> have to throw int exceptions if you are doing performance
> comparison. Also, remember, you have to do "if (return_code ==
> failed)" type junk at every function call if you are using error
> return values instead of exceptions (otherwise you aren't doing any
> error handling which is an unfair test and bad programming). You
> don't have to do that when throwing exceptions. You only need one
> catch clause no matter how deep the call stack is.
Right.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
> David Abrahams wrote:
>> No, there's nothing magical about array construction.
>>
> The point is that at the same PC value you have different number of
> objects
> to be destructed.
>
> Do you agree? (yes/no)
I agree that it's true, but it's totally irrelevant. The same
applies to any simple recursive function (or any function that may
eventually appear twice in the same call stack):
void f(int x)
{
if (x == 0) return;
T y;
f(x-1);
}
The information about how many Ts need to be destroyed is stored in
the stack and registers, **exactly like in the case of array
construction**. There is nothing at all special about the array case.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> On Jun 7, 11:06 am, Jerry Coffin <jcof...@taeus.com> wrote:
>
>> Here's some data you might find interesting. These are results from
>> (part of) Joe Orost's bench++.
>>
>> Result set 1: Microsoft C++ x64 edition [ what's this? ]
>> [...]
>> Result set 2: g++ 3.4 (mingW)
>> [...]
>> Result set 3: Microsoft VC++ 7.1 with SSE instructions enabled:
>> [...]
>>
>> These were all run on the same machine. Of course, others will have
>> faster or slower machines than mine, but that's likely to affect all the
>> results by approximately a constant factor. This is probably semi-
>> representative of what to expect with a reasonably current desktop
>> machine of the x86 variety. My guess is that the ratio between
>> throw/catch and setjmp/longjmp will remain pretty large on most other
>> machines as well.
>
> Testing GCC 4.2.0, preferably on linux, would probably be more
> interesting than 3.4.0 on Windows.
Yes; IIUC no GCC implementation for windows has ever used the new GCC
ABI with its range-table-based EH implementation, which has been in
place on Linux at least since GCC 4 (I think actually since GCC 3).
Also I don't think the tests shown are measuring what most of us
should care about most of the time: the cost of EH on the non-error
path, i.e. when no errors actually occur -- as compared with a program
with the same error-handling semantics but not using EH.
> It may also be interesting to test the Intel compiler, since they're
> the ones who came up with the famous Itanium ABI for exception
> handling.
IIUC it was a joint effort among a large group of vendors.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> Probably the reason people think throwing exceptions is slow, is they
> are throwing std::string or even more complex exception objects. Sure,
> if you have to construct a complex exception object it's going to be
> slower but that's not a fair test. If you are going to compare
> returning an int and throwing an exception, you have to also use the
> same atom of error information.
Thats probably why our little arm processor is coughing o much when
throwing in a loop in comparison with return values.... I should have
realized, but it made no significant difference on the x86. Come to
think of it, I can't think of an elegant solution, but it would have
been nice if one had the ability to catch a standard lightweight
exception that just wrapped integers. The fact that standard
exceptions use strings contribute greatly to the fact that most
exceptions are married to strings. Marrying string and integer
exceptions elegantly in the standard (with all respect) would have
been a great feat, not? Catching a standard exception almost by
implication requires string processing.
Regards,
Werner
> Hyman Rosen wrote:
> > Now, when the construction code calls f recursively, p and cc are in
> > registers which the recursive call will need to reuse. Therefore, they
> > must be saved, either before the call or at the beginning of f.
> >
> This statement isn't quite correct. You should say "they must be
> saved
> somewhere in the f()'s call chain" if the chain modifies R1 value at all.
> Look, if cc lives in R1 and f() calls g() then R1 can be saved in
> g().
> Moreover, if f() is really complicated and the full possible call
> chain is
> f()->g()->h(), then R1 can be saved in f(), g(), h() or not saved at all!
So it gets restored whereever it is saved. Doesn't make a difference. It
would be just the same if the functions were returning the normal way.
> The following real case is almost unlimited call chain where some
> functions
> are extern "C" (which are opaque to the EH runtime except for their return
> address and number of locally allocated bytes)...
As Hyman said, extern "C" functions are just C++ with different linkage.
Real C functions ...
Well, put it this way. First, to push the register saving as far down the
call stack as you want, the compiler has to analyze all the functions in
question, so that it knows where the register is overwritten. (It must
also be aware of all other places the function that overwrites it is
called, which is actually impossible if the function is visible outside
the unit the compiler analyzes, or it might pessimize the function against
all other callers. If, on the other hand, the function is called only in
this single place, the compiler might as well just inline it. All in all,
I doubt the optimization is practical. But let's leave that aside.)
Returning to the point, if the compiler wants to leave the saving of the
register until the C function, it must be able to compile C and C++ code
at the same time. If it can do that, it can also make the C code
exception-aware. (Note that in the table-based model, no code is actually
generated within the function for exception handling, so this would be
fully compatible with C callers.)
So either the compiler can make the optimization. In that case it can make
also make it in the presence of exceptions.
Or it can't make it. But that has nothing to do with exceptions, merely
with code not available to the optimizer.
Sebastian Redl
> Extern "C" is just linkage - such code is regular C++ and deals with
> exceptions like any other C++ code. If you're talking about plain C
> code and trying to pass exceptions through such functions, that's
> obviously not in the scope of the standard.
>
But it's very important real-world issue. A lot of C++ implementations (all
of them?) describe how to set your stack frame up in an assembly function to
be unwinded by the EH runtime. But I haven't yet seen any requirements how
to store the caller's R1 count to let it be found by the runtime...
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
> but it's totally irrelevant.
>
No, it's not.
> The same applies to any simple recursive function (or any function
> that may eventually appear twice in the same call stack):
>
No. At least not in this sense.
Look, (roughly speaking) the unwinding runtime deals with one stack frame
at a time:
1. In the case of recursive function one stack frame together with some PC
value points to known number of objects to be destructed.
2. But in the case of array construction one stack frame+PC points to
_unknown_ number of objects!
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
I thought I did that already, but OK, here it is.
Let's go back to that recursive array construction
with a slight change, f now has two arrays.
extern volatile bool condition;
exetrn voltaile bool die;
extern volatile int s;
struct A { A(); ~A() { s = 0; } };
void f() { A arr1[N]; A arr2[N]; /* ... */ }
A::A() { if (condition) f(); if (die) throw 0; }
And let's say that through the vagaries of the code
generator, the first array construction uses a pair
of registers to hold the address and count, but the
second one only uses one register for the count. The
code will look like this:
f:
register A *p1 = stackalloc(N * sizeof(A));
register int cc1;
for (cc = 0; cc < N; ++cc) {
if (condition) { push p1reg; push cc1reg; Label1: f(); pop cc1reg;
pop p1reg; }
if (die) throw 0;
}
A *p2 = stackalloc(N * sizeof(A));
register int cc2;
for (cc = 0; cc < N; ++cc) {
if (condition) { push cc2reg; Label2: f(); pop cc2reg; }
if (die) throw 0;
}
Now there are two places where unwinding brings us back inside of f,
namely Label1 and Label2. The compiler generates separate handlers
for each. In the following, assume that stack unwinding has popped
everything up to and including the return address of f's caller. That
return address is used to search a table for one of these handlers:
Handler_For_Label1:
pop cc1reg; pop p1reg;
while (cc1-- > 0) (p1 + cc1)->~A();
// continue unwinding to caller of f
Handler_For_Label2:
pop cc2reg;
while (cc2-- > 0) (p2 + cc2)->~A(); // p2 is local with respect to the
stack
cc2 = N;
while (cc2-- > 0) (p1 + cc2)->~A(); // p1 is local with respect to the
stack
// continue unwinding to caller of f
If the compiler is clever enough, it can probably combine the code
of the two parts of the handlers where the first array is destructed.
> A lot of C++ implementations (all of them?) describe how to set your stack
frame
> up in an assembly function to be unwinded by the EH runtime.
If that's true, if there's a standard way to set things up in a
function for unwinding, then the ABI is using callee-saves, not
caller-saves. That standard way of unwinding then also includes
popping saved registers off the stack. If the C ABI matches, then
exceptions can propagate invisibly through C functions.
> But I haven't yet seen any requirements how to store the caller's R1 count
to let
> it be found by the runtime...
If the callee saves in the function preamble, then it's just popped
off by the regular unwind. If it's using caller-saves, with different
save patterns at each call site, then the C++ compiler will know that
the C code may clobber registers without the EH mechanism able to
restore them while unwinding the C stack, and can just save them in
caller-saves fashion before calling the C code.
> David Abrahams wrote:
>> I agree that it's true,
>>
> Good :)
>
>> but it's totally irrelevant.
>>
> No, it's not.
>
>> The same applies to any simple recursive function (or any function
>> that may eventually appear twice in the same call stack):
>>
> No. At least not in this sense.
>
> Look, (roughly speaking) the unwinding runtime deals with one stack
frame
> at a time:
> 1. In the case of recursive function one stack frame together with
some PC
> value points to known number of objects to be destructed.
> 2. But in the case of array construction one stack frame+PC points
to
> _unknown_ number of objects!
OK, for the last time, and then I'm done: the number of objects is not
unknown. It's known at runtime and represented in an implicit
automatic variable. The issues are no different from the issues in
this case, where there is no array.
void f()
{
for (int i = 0; i < 10; ++i)
{
try
{
c(i); // might throw
}
catch(...)
{
while (i > 0)
d(--i);
throw;
}
}
}
Each call to c corresponds to an element construction in the array
case. Each call to d corresponds to an element destruction in the
array case. The value of i needs to be available in order to do the
recovery (just as it would be if some mechanism other than EH was
being used to do error handling). The compiler internally translates
array construction to something very much like the above. The issues
are the same.
There's no magic here.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
Come on Sergey!
The compiler generates the code for both the construction and the
destruction of the array elements. When it generates the destruction
code for a specific PC range, it very well knows where the
corresponding construction code stores its count - like register ECX.
It just has to pick it up, and start counting downwards!
Bo Persson
Your "recursive function" example is flawed.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
> > A lot of C++ implementations (all of them?) describe how to set your
stack
> > frame up in an assembly function to be unwinded by the EH runtime.
>
> If that's true, if there's a standard way to set things up in a
> function for unwinding, then the ABI is using callee-saves, not
> caller-saves.
>
Hmm... I don't see why the callee-saves ABI is a must in this case.
E.g. the requirement can just be
push bp
move bp, sp
i.e. neither callee-saves no caller-saves are supposed.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
I think it's time for me to give up. I (and other posters) have
demonstrated over and over again that this isn't true, except
for the limited case where exceptions must pass through code
which is not C++ and which has not been compiled to know how to
deal with that. If you still cannot understand why that is, then
I guess we will just have to leave it at that.
One last time: At any given point in the code, any kind of counter
that is being used is in a known place, be it in a global variable
or in the stack frame. At a point in the code where a function is
to be called, those counters will either be saved by the caller, in
which case the exception handling code will retrieve them from the
saved location using a dedicated handler built for that point, or
the counters will be saved in the called function, in which case the
exception handling code will restore them during the stack unwind of
the called function, using a variety of dedicated handlers there if
that code has different ways in which it saves the registers.
Look at an assembly language listing of a function. At any point in
that code, it has some set of registers (possibly empty) preserved
in known locations that it will want to restore before returning. This
is true even when there are no exceptions involved. The compiler writes
an exception handler dedicated to one particular point in the code which
knows how to restore the registers that are preserved at that point, for
every point in the code where a function is called. Then it builds a
table mapping code addresses to exception handlers. When the unwind
mechanism sees that it wants to go back to some point in the code, it
looks up the handler for that point, and that handler restores the saved
registers. If it happens that we're using callee-saves, and the callee
doesn't use that register and doesn't save it, then that's fine. The
dedicated handler for that call point won't do anything with the register,
which will still have its old value.
> except
> for the limited case where exceptions must pass through code
> which is not C++ and which has not been compiled to know how to
> deal with that.
>
Is this a rather theoretical case? I think it is not.
> If you still cannot understand why that is, then
> I guess we will just have to leave it at that.
>
I understand that IN THEORY the runtime can unwind all the execution
path
and restore any counter from any possible location (even by unXORing it
out). But IN PRACTICE compilers save the counter in its own stack frame
because this approach allows to build real-world EH runtime of acceptable
quality (QoI issue).
Once again, my points:
1. PC value+range table is insufficient.
2. Due to real-world QoI issues EH-aware compilers have to pin the counters
to the corresponding stack frames. While having EH disabled they can
delegate the register savings to the callees (not callers). This is an
unavoidable EH runtime overhead.
Yes, IN THEORY programs are written in standard C++ and no extern
"C"
assembly functions allowed. In theory one can build a sophisticated compiler
that is able to unwind the execution path (not only local objects to be
destructed).
But this theory is unreachable. You'll never see this magic unwinder
in
practice so I continue to say: PC+table is insufficient; EH imposes certain
runtime overhead even if exceptions aren't thrown.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
> Hyman Rosen wrote:
>> > I'm trying to say that having EH enabled the caller _must_
>> > save the counter (i.e. the compiler looses certain optimizations).
>>
>> I think it's time for me to give up. I (and other posters) have
>> demonstrated over and over again that this isn't true,
Butting in on my way out the door, because my position is being
misrepresented below...
Of course it depends what you're comparing with. If you are going to
handle failures with error codes or any other mechanism then the
counter needs to be saved too, so that you can perform the same
recovery actions (destroying the constructed elements requires
knowledge of which elements were constructed). The only way you can
get optimization that are impossible with a range-table based approach
is if you really don't need the counter at all (e.g. there's no
error-handling or recovery). The "limited case where exceptions must
pass through code which is not C++ and which has not been compiled to
know how to deal with that" is just a special case of "no error
handling."
> Not exact, unfortunately.
>
> At least one poster has been "demonstrating" that PC+range table is
> enough.
No. I have been clearly saying that PC+range table is not enough by
itself and that you also need the contents of the stack and registers,
in which case any information that would be needed to recover under
non-EH error handling schemes is no less available when EH is used.
> This statement is obviously false (in the case of array
> construction).
You defeated an argument I was never making. Rather an easy target,
no?
Ciao!
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
I didn't see the article, but at least on Windows, having EH enabled
does add some cost to successful paths. The reason is because EH on x86
Windows is implemented using Microsoft's "Structured Exception Handling"
scheme which requires:
1) a special stack frame
2) incrementing/decrementing a special 'scope index' variable when going
in and out of try blocks
I think this cost was reduced for SEH on x64, at the cost of much slower
execution of catch blocks. While the modern trend is to accept slow
exception paths, such does preclude using the hardware to implement
write barriers in gc implementations.
--------------
Walter Bright
http://www.digitalmars.com C, C++, D programming language compilers
http://www.astoriaseminar.com Extraordinary C++
--
You may want to try compiling this code and looking at
the assembly language.
enum { N = 1000 };
extern volatile bool a, b, c;
extern "C" { void f(); void g(); }
struct m {
m() { if (a) { f(); g(); } if (b) throw 0; }
~m() { c = true; }
};
void f() { m M[N]; }
int main() { try { m M; } catch (int r) { return r; } }
I built on a Solaris x86 machine with g++.5.10 (GCC) 4.1.0,
using 'g++ -O3 -S -fno-inline -fomit-frame-pointer' (the no
inline is so that I can follow the calls more easily).
Anyway, this really is it for me. It's fruitless to continue this.
> at least on Windows, having EH enabled does add some cost to
> successful paths. The reason is because EH on x86 Windows is
> implemented using Microsoft's "Structured Exception Handling" scheme
> which requires:
>
> 1) a special stack frame
>
> 2) incrementing/decrementing a special 'scope index' variable when going
> in and out of try blocks
It does not have to be implemented that way on Windows. That's just
how Microsoft did it on 32-bit windows, and several vendors naturally
followed suit. I don't believe Cygwin GCC integrates with SEH (though
it might - their mechanism is just about as slow as SEH), and
Metrowerks on windows had an option to select "zero-overhead" or
"SEH-compatible" exception handling.
> I think this cost was reduced for SEH on x64,
At least according to the Visual C++ devs, they implemented a
range-table based approach for x64.
> at the cost of much slower execution of catch blocks.
Slower unwinding, actually. The catch block itself is just regular
C++ code.
> While the modern trend is to accept slow exception paths, such does
> preclude using the hardware to implement write barriers in gc
> implementations.
Care to say more? I don't follow that.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
David Abrahams wrote:
> Of course it depends what you're comparing with.
>
I'm trying to compile g_functions.cpp (Message-ID:
<46691762...@iobox.com>) that is intentionally designed to be
exception-neutral. In particular, gI() functions build their arrays from
objects that don't throw exceptions. Nevertheless, the compiler has to
generate unoptimal code to allow exceptions to propagate thru.
> If you are going to
> handle failures with error codes or any other mechanism then the
> counter needs to be saved too, so that you can perform the same
> recovery actions (destroying the constructed elements requires
> knowledge of which elements were constructed).
>
The point is that the middle layer (living in g_functions.cpp) doesn't have
to care about failures you're talking about. By design.
> The only way you can
> get optimization that are impossible with a range-table based approach
> is if you really don't need the counter at all (e.g. there's no
> error-handling or recovery). The "limited case where exceptions must
> pass through code which is not C++ and which has not been compiled to
> know how to deal with that" is just a special case of "no error
> handling."
>
I can't buy this, sorry. "No EH" doesn't mean "no error handling".
I believe a lot of extern "C" functions return certain error codes. You
have to check these error codes even with enabled EH.
> > At least one poster has been "demonstrating" that PC+range table is
> > enough.
>
> No. I have been clearly saying that PC+range table is not enough by
> itself and that you also need the contents of the stack and registers,
> in which case any information that would be needed to recover under
> non-EH error handling schemes is no less available when EH is used.
>
Good, very good! :)
DO YOU PROMICE not to say something like "The range table approach uses the
return addresses on the program stack and program counter to do all the
dynamic bookkeeping" in future? ;)
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> { Please fit your text in 70 columns or so. Note that a tab character
> at the beginning of a line takes up 8 columns in most terminals.
> Text manually reformatted. -mod }
>
> David Abrahams wrote:
>> Of course it depends what you're comparing with.
>>
>
> I'm trying to compile g_functions.cpp (Message-ID:
> <46691762...@iobox.com>) that is intentionally designed to be
> exception-neutral.
Irrelevant. With exceptions off those functions are incapable of
handling errors reported by h().
> In particular, gI() functions build their arrays from objects that
> don't throw exceptions. Nevertheless, the compiler has to generate
> unoptimal code to allow exceptions to propagate thru.
If you handle potential errors from h() via some other mechanism, it
also needs to generate "unoptimal" code. There is no free lunch. If
you want the semantics of handling potential errors, you need to pay a
certain price to do so, EH or no EH. It just so happens that using EH
gives the compiler a little more information about which code to favor
when optimizing.
>> If you are going to handle failures with error codes or any other
>> mechanism then the counter needs to be saved too, so that you can
>> perform the same recovery actions (destroying the constructed
>> elements requires knowledge of which elements were constructed).
>>
>
> The point is that the middle layer (living in g_functions.cpp)
> doesn't have to care about failures you're talking about. By design.
Then "by design" it can't call a function like h() that can
potentially fail.
>> The only way you can get optimization that are impossible with a
>> range-table based approach is if you really don't need the counter
>> at all (e.g. there's no error-handling or recovery). The "limited
>> case where exceptions must pass through code which is not C++ and
>> which has not been compiled to know how to deal with that" is just
>> a special case of "no error handling."
>>
>
> I can't buy this, sorry. "No EH" doesn't mean "no error handling".
> I believe a lot of extern "C" functions return certain error codes. You
> have to check these error codes even with enabled EH.
You obviously think I'm saying something I'm not.
>> > At least one poster has been "demonstrating" that PC+range
>> > table is enough.
>>
>> No. I have been clearly saying that PC+range table is not enough by
>> itself and that you also need the contents of the stack and registers,
>> in which case any information that would be needed to recover under
>> non-EH error handling schemes is no less available when EH is used.
>>
>
> Good, very good! :)
>
> DO YOU PROMICE not to say something like "The range table approach uses
the
> return addresses on the program stack and program counter to do all the
> dynamic bookkeeping" in future? ;)
No! Of course I don't promise to explicitly mention the things that
are always there, like the stack, registers, CPU, and
dynamically-allocated memory. (**) I take all those things for
granted, and I think it's reasonable to assume that any sensible
reader will as well.
Believe it or not, EH won't work if your CPU disappears!
I don't think that headline is going to cause a sensation anywhere,
sorry.
(**) dynamic memory will be needed to recover from the construction of a
std::vector<std::string> in exactly the same way that this counter is
needed to recover from the construction of an array.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
bool h(something* ptr)
{
// ...
if (failed) return false;
// ...
}
And the caller gN() has to locally check for local errors:
bool gN(something2* ptr2)
{
// ...
if (!(h(ptr))) return false;
// ...
}
Only the errors that can't be hadled locally are reported by h()
using
exceptions.
> If you handle potential errors from h() via some other mechanism, it
> also needs to generate "unoptimal" code.
>
Are you kidding?
Do you really like that brain-damaged Java approach where EVERY
error is an
excepion?!
> > The point is that the middle layer (living in g_functions.cpp)
> > doesn't have to care about failures you're talking about. By design.
>
> Then "by design" it can't call a function like h() that can
> potentially fail.
>
Yes, it can.
> > DO YOU PROMICE not to say something like "The range table approach uses
> > the return addresses on the program stack and program counter to do all
> > the dynamic bookkeeping" in future? ;)
>
> No! Of course I don't promise to explicitly mention the things that
> are always there, like the stack, registers, CPU, and
> dynamically-allocated memory.
>
David, your statement above is obviously misleading. That's why I
started
to contribute to this subthread.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
No way. I much prefer the C approach where developers fail to check
return codes (always) resulting in hard to find errors, memory leaks,
resource leaks, incomplete error information like "Program Failed,
Reason: 2", uninitialized pointers corrupting the entire program
randomly, random error philosophy like Joe's code where "return NULL
if failure, or 27 if success, or -1 for failure, or 36 for true or
HERROR if something or other", functions where the majority of the
code is "if (error) do this" obscuring the true intent of the function
and other much more sane C approaches to error handing. Being force to
handle errors is pure bondage and discipline. I might as well program
in BASIC or PASCAL
Whoever came up with exceptions must have been a real idiot. Who in
their right minds would want good error handling? It's no wonder the
Mars Explorer miscalculated the distances. They must have been using
exceptions in that code.
Right, but it's nice being compatible with the standard way the
operating system works. For example, if a hardware exception occurs, you
get an SEH which can be caught by C++. It's nice being compatible with
DLLs that throw standard exceptions. If you invent your own ABI, you're
compatible with nobody.
> That's just
> how Microsoft did it on 32-bit windows, and several vendors naturally
> followed suit.
Digital Mars C++ certainly did it that way <g>. DMC++ uses range tabled
EH for non-Windows executables (like 32 bit DOS). Digital Mars D uses
SEH for Windows and range tables for Linux.
>> at the cost of much slower execution of catch blocks.
>
> Slower unwinding, actually. The catch block itself is just regular
> C++ code.
Right.
>> While the modern trend is to accept slow exception paths, such does
>> preclude using the hardware to implement write barriers in gc
>> implementations.
> Care to say more? I don't follow that.
For a generational GC, you can avoid scanning sections of memory that
you know haven't changed since the last time it was scanned. This is
done by implementing a "write barrier", which marks the section as
"changed and needs rescanning" when it is written to. One way to do this
is to instrument every write instruction. Another way is to mark those
sections' pages as "read only". Then, the hardware throws a "write
fault" exception when an instruction tries to write to it. You set up a
handler that intercepts the write fault, logs the faulting page as
"changed and need rescanning", then turns on the write enable bit for
the page, and restarts the instruction.
This is not practical if the exception unwinding is slow.
-------------
Walter Bright
http://www.digitalmars.com C, C++, D programming language compilers
http://www.astoriaseminar.com Extraordinary C++
--
> David Abrahams wrote:
>> > I'm trying to compile g_functions.cpp (Message-ID:
>> > <46691762...@iobox.com>) that is intentionally designed to be
>> > exception-neutral.
>>
>> Irrelevant. With exceptions off those functions are incapable of
>> handling errors reported by h().
>>
> Not really.
> h() reports the errors that can be handled _locally_ via its return
> code:
>
> bool h(something* ptr)
> {
> // ...
> if (failed) return false;
> // ...
> }
>
> And the caller gN() has to locally check for local errors:
>
> bool gN(something2* ptr2)
> {
> // ...
> if (!(h(ptr))) return false;
> // ...
> }
>
> Only the errors that can't be hadled locally are reported by h()
> using
> exceptions.
And if the g_functions are not able to deal with exceptions passing
through them, then that mechanism of reporting errors is no longer
available to h().
>
>> If you handle potential errors from h() via some other mechanism, it
>> also needs to generate "unoptimal" code.
>>
> Are you kidding?
> Do you really like that brain-damaged Java approach where EVERY
> error is an
> excepion?!
I don't know about David, but my answer is No.
But by using status codes to report problems, you also constrain the
compiler to generate less optimal code.
Because th error path is heavily intermixed with the non-error path, the
compiler is unable to apply optimisations that make the non-error path
faster, by making the error path slower.
>
>> > The point is that the middle layer (living in g_functions.cpp)
>> > doesn't have to care about failures you're talking about. By
>> > design.
>>
>> Then "by design" it can't call a function like h() that can
>> potentially fail.
>>
> Yes, it can.
But only by reporting the failure with a status code.
There are basically two mechanisms by which an exception can go from the
throw point to the catch handler.
The first is equivalent to a longjmp() (with proper stack unwinding once
the catch handler is reached). If the functions in between the throw
point and the catch are 'exception unaware', then these functions can
not do a proper cleanup of their local objects. The lifetime of those
objects will simply be ended without destruction, with a high
probability of undefined behaviour in the process.
The second mechanism is that each stack-frame is unwound explicitly,
until a suitable handler is located. In this situation, if a function
is 'exception unaware', then unwinding the stack-frame may not be
possible at all. There is no guarantee that you will end up in a catch
handler, and it might even result in the execution of arbitrary code.
Bart v Ingen Schenau
--
a.c.l.l.c-c++ FAQ: http://www.comeaucomputing.com/learn/faq
c.l.c FAQ: http://www.eskimo.com/~scs/C-faq/top.html
c.l.c++ FAQ: http://www.parashift.com/c++-faq-lite/
> For a generational GC, you can avoid scanning sections of memory that
> you know haven't changed since the last time it was scanned. This is
> done by implementing a "write barrier", which marks the section as
> "changed and needs rescanning" when it is written to. One way to do this
> is to instrument every write instruction. Another way is to mark those
> sections' pages as "read only". Then, the hardware throws a "write
> fault" exception when an instruction tries to write to it. You set up a
> handler that intercepts the write fault, logs the faulting page as
> "changed and need rescanning", then turns on the write enable bit for
> the page, and restarts the instruction.
>
> This is not practical if the exception unwinding is slow.
That's not a correct use of exceptions.
That's a hack.
Exceptions are meant for exceptions, nothing more, nothing less.
On Sat, 16 Jun 2007, Walter Bright wrote:
> Right, but it's nice being compatible with the standard way the
> operating system works. For example, if a hardware exception occurs, you
> get an SEH which can be caught by C++.
Yes, but as I recall, this "feature" of VC++6 was attacked fiercely and
removed from VC++.Net 2002. So it was apparently not found to be so nice
after all.
As for cross-DLL exception throwing, that's an illusion anyway. Pretty
much every C++ compiler has its own ABI, at least far enough that code
from two compilers cannot interact on the C++ level, having instead to
rely on C APIs. Exceptions are one reason for this incompatibility, but
not the only one. RTTI information layout is another.
But if two modules cannot interact in C++ terms anyway, there's hardly a
point in making their exception handling mechanisms compatible, is there?
If two compilers really want to interact, they can synchronize their
methods. Hopefully they'll agree on good methods.
> For a generational GC, you can avoid scanning sections of memory that
> you know haven't changed since the last time it was scanned. This is
> done by implementing a "write barrier", which marks the section as
> "changed and needs rescanning" when it is written to. One way to do this
> is to instrument every write instruction. Another way is to mark those
> sections' pages as "read only". Then, the hardware throws a "write
> fault" exception when an instruction tries to write to it. You set up a
> handler that intercepts the write fault, logs the faulting page as
> "changed and need rescanning", then turns on the write enable bit for
> the page, and restarts the instruction.
>
> This is not practical if the exception unwinding is slow.
It is my understanding that this mechanism relies on restart semantics,
which are not supported by C++ anyway. Furthermore, restart semantics do
not lead to stack unwinding. (If the stack is unwound, how could the
function resume running?) Only the lookup of an exception handler is
needed.
Thus, you could, for example, split the range table in two parts: one
containing basic information (where the exception handlers are, where to
find the return address) and one containing the advanced information (how
to unwind each stack frame).
The first table should be quite small and can thus be held in memory and
be fast to access. Since this is all that is needed for restart semantics,
the write protection trick should work.
Sebastian Redl
I'm not sure what way is best, but it's possible to
generate some of the checks that would otherwise be
omitted. http://www.seventy7.homelinux.net/comp/Msgs.h
> Reason: 2", uninitialized pointers corrupting the entire program
> randomly, random error philosophy like Joe's code where "return NULL
> if failure, or 27 if success, or -1 for failure, or 36 for true or
> HERROR if something or other", functions where the majority of the
> code is "if (error) do this" obscuring the true intent of the function
> and other much more sane C approaches to error handing. Being force to
> handle errors is pure bondage and discipline. I might as well program
> in BASIC or PASCAL
>
I consider discipline a good thing. I think Abrahams said
that exceptions allow you to check at one level rather
than N levels. That's true, but what about the context? Context is
everything. When an error occurs I generally
need to know how we got there and that isn't the time to
add code to provide context. Some problems are so
difficult to reproduce you don't have that luxury.
> Whoever came up with exceptions must have been a real idiot. Who in
> their right minds would want good error handling?
I don't think Sergey was saying that. I think there is
a place for exceptions, but they weren't a silver bullet.
Brian Wood
Ebenezer Enterprises
www.webebenezer.net
--
Well, I think using C++ exceptions for this would be unfortunate (not
to mention it would not be possible with them). C++, Java, SEH
exceptions
are mostly non-local returns. What you want are some more hardwarish
exceptions, eg. in case of something, call something else for me, then
possibly retry. They don't do any unwinding (why should they?), and
you
don't want any. You want to set this globally, regardles of the call
stack.
Regards
Jiri Palecek
It would be really nice if C++ provided a portable way to access a stack
trace describing all the calls that got unwound between where the exception
was thrown and where it was caught. Certainly the information is available
to the compiler.
Yeah, I know, it would be expensive to implement, and the memory management
would be complicated, etc, etc, but it sure would make debugging easier
sometimes.
It existed long before VC6. I don't know how .net implements exception
handling.
> As for cross-DLL exception throwing, that's an illusion anyway. Pretty
> much every C++ compiler has its own ABI, at least far enough that code
> from two compilers cannot interact on the C++ level, having instead to
> rely on C APIs. Exceptions are one reason for this incompatibility, but
> not the only one. RTTI information layout is another.
Digital Mars C++'s ABI is close enough to Microsoft's that you can
interchange exceptions, object layout and function calls. Even the name
mangling matches. There is also considerable interest in binary
compatibility across DLLs.
> But if two modules cannot interact in C++ terms anyway, there's hardly a
> point in making their exception handling mechanisms compatible, is there?
Microsoft's structured exception handling mechanism is language independent.
> If two compilers really want to interact, they can synchronize their
> methods. Hopefully they'll agree on good methods.
>
>> For a generational GC, you can avoid scanning sections of memory that
>> you know haven't changed since the last time it was scanned. This is
>> done by implementing a "write barrier", which marks the section as
>> "changed and needs rescanning" when it is written to. One way to do this
>> is to instrument every write instruction. Another way is to mark those
>> sections' pages as "read only". Then, the hardware throws a "write
>> fault" exception when an instruction tries to write to it. You set up a
>> handler that intercepts the write fault, logs the faulting page as
>> "changed and need rescanning", then turns on the write enable bit for
>> the page, and restarts the instruction.
>>
>> This is not practical if the exception unwinding is slow.
>
> It is my understanding that this mechanism relies on restart semantics,
> which are not supported by C++ anyway. Furthermore, restart semantics do
> not lead to stack unwinding. (If the stack is unwound, how could the
> function resume running?) Only the lookup of an exception handler is
> needed.
You're right, it does rely on restart semantics, which are a capability
of Microsoft's structured exception handling. The gc comment was not
directed at C++, but at a casualty of redesigning SEH.
> Thus, you could, for example, split the range table in two parts: one
> containing basic information (where the exception handlers are, where to
> find the return address) and one containing the advanced information (how
> to unwind each stack frame).
> The first table should be quite small and can thus be held in memory and
> be fast to access. Since this is all that is needed for restart semantics,
> the write protection trick should work.
SEH as it is is barely fast enough to do the write-barrier thing (I
know, I've tried it out). Any slowdowns in it will make it impractical.
SEH doesn't require a range table, it can access things directly.
-------------
Walter Bright
http://www.digitalmars.com C, C++, D programming language compilers
http://www.astoriaseminar.com Extraordinary C++
--
Using hardware write barriers in this manner is a well-known and
well-traveled path in implementing garbage collectors.
See "Garbage Collection" by Richard Jones and Rafael Lins
Hardware exceptions are also used for all kinds of other things, like
demand paged virtual memory, for example, and other forms of
virtualization. These form the basis of modern operating system design,
and are not a hack. The hardware is specifically designed to support
this, and SEH was specifically designed to make this stuff available to
user programs.
-------------
Walter Bright
http://www.digitalmars.com C, C++, D programming language compilers
http://www.astoriaseminar.com Extraordinary C++
--
I think the point here is that sometimes the context is reversed. The
low level routine that runs into a problem might not have enough
context to handle it. Several levels up in the program, some other
routine might.
Consider a routine doing some calculations. As they are rather
expensive to perform, it might be a good idea to save the result for
later. So the routine managing the calculations calls a general
caching routine, which calls a specific storage strategy routine,
which calls insert on a std::vector, which might call operator new to
allocate additional space.
If the allocation fails, what should operator new do? It doesn't have
a clue! Neither has the vector.
The top level routine, on the other hand, might consider throwing out
some other values to make room, or just drop the current value and
recalculate it later. It has the proper context to decide.
Bo Persson
On Sun, 17 Jun 2007, Walter Bright wrote:
> It existed long before VC6. I don't know how .net implements exception
> handling.
If you're referring to C++: the same way, but catch(...) no longer
catches anything but true C++ exceptions.
The .Net CLR is a completely different beast, considering that it's
running in a virtual machine with lots of execution state information.
> Digital Mars C++'s ABI is close enough to Microsoft's that you can
> interchange exceptions, object layout and function calls. Even the name
> mangling matches. There is also considerable interest in binary
> compatibility across DLLs.
Interesting. But I suppose any kind of exception handling could be made
the same across compilers.
> Microsoft's structured exception handling mechanism is language independent.
True. But it's not the only way of implementing C++ exceptions.
I refer back to my earlier statement:
> > If two compilers really want to interact, they can synchronize their
> > methods. Hopefully they'll agree on good methods.
> You're right, it does rely on restart semantics, which are a capability
> of Microsoft's structured exception handling. The gc comment was not
> directed at C++, but at a casualty of redesigning SEH.
This may be, of course. But then, the newsgroup is about C++, not SEH. If
I interpreted an earlier comment correctly, then the x64 code generator of
Visual Studio no longer uses SEH to implement C++ exceptions.
> SEH as it is is barely fast enough to do the write-barrier thing (I
> know, I've tried it out). Any slowdowns in it will make it impractical.
> SEH doesn't require a range table, it can access things directly.
I wonder if it would be possible to actually speed up SEH by using such a
separate, small table. But I don't understand the issues well enough to
make a judgement, and it's irrelevant anyway: I don't see a way to make
this compatible with existing code.
Sebastian Redl
> On Sat, 16 Jun 2007, Walter Bright wrote:
>
>> Right, but it's nice being compatible with the standard way the
>> operating system works. For example, if a hardware exception occurs, you
>> get an SEH which can be caught by C++.
>
> Yes, but as I recall, this "feature" of VC++6 was attacked fiercely and
> removed from VC++.Net 2002. So it was apparently not found to be so nice
> after all.
Yes, more about the reasons why here:
http://groups.google.com/group/comp.lang.c++.moderated/browse_frm/thread/2d5c923a8f6bf710
> As for cross-DLL exception throwing, that's an illusion
> anyway. Pretty much every C++ compiler has its own ABI, at least far
> enough that code from two compilers cannot interact on the C++
> level, having instead to rely on C APIs. Exceptions are one reason
> for this incompatibility, but not the only one. RTTI information
> layout is another.
Looking at it another way, SEH is Microsoft's standard ABI for
exceptions on 32-bit windows. I don't have a problem with that (other
than that it's dog-slow and hardware exceptions are tied into it).
> But if two modules cannot interact in C++ terms anyway, there's
> hardly a point in making their exception handling mechanisms
> compatible, is there? If two compilers really want to interact,
> they can synchronize their methods. Hopefully they'll agree on good
> methods.
Unfortunately in this case, MS got to define the platform standard
long ago when nobody knew better ;-)
>> For a generational GC, you can avoid scanning sections of memory that
>> you know haven't changed since the last time it was scanned. This is
>> done by implementing a "write barrier", which marks the section as
>> "changed and needs rescanning" when it is written to. One way to do this
>> is to instrument every write instruction. Another way is to mark those
>> sections' pages as "read only". Then, the hardware throws a "write
>> fault" exception when an instruction tries to write to it. You set up a
>> handler that intercepts the write fault, logs the faulting page as
>> "changed and need rescanning", then turns on the write enable bit for
>> the page, and restarts the instruction.
>>
>> This is not practical if the exception unwinding is slow.
>
> It is my understanding that this mechanism relies on restart semantics,
> which are not supported by C++ anyway. Furthermore, restart semantics do
> not lead to stack unwinding. (If the stack is unwound, how could the
> function resume running?) Only the lookup of an exception handler is
> needed.
Exactly.
> Thus, you could, for example, split the range table in two parts: one
> containing basic information (where the exception handlers are, where to
> find the return address) and one containing the advanced information (how
> to unwind each stack frame).
> The first table should be quite small and can thus be held in memory and
> be fast to access. Since this is all that is needed for restart semantics,
> the write protection trick should work.
I don't even think you need your EH tables for that purpose. IIUC
it's just standard hardware exception handling, done the same way that
page faults are handled.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
>>> While the modern trend is to accept slow exception paths, such does
>>> preclude using the hardware to implement write barriers in gc
>>> implementations.
>> Care to say more? I don't follow that.
>
> For a generational GC, you can avoid scanning sections of memory that
> you know haven't changed since the last time it was scanned. This is
> done by implementing a "write barrier", which marks the section as
> "changed and needs rescanning" when it is written to.
Oh, sure. I thought you meant "memory barrier."
> One way to do this is to instrument every write instruction. Another
> way is to mark those sections' pages as "read only". Then, the
> hardware throws a "write fault" exception
Whoa, whoa, whoa. The hardware "raises" a _hardware_ "write fault"
exception. There's no reason that should be connected in any way to
C++ EH, and in fact there are really good reasons that it shouldn't be
so connected.
> when an instruction tries to write to it. You set up a handler that
> intercepts the write fault, logs the faulting page as "changed and
> need rescanning", then turns on the write enable bit for the page,
> and restarts the instruction.
>
> This is not practical if the exception unwinding is slow.
You can do all that stuff without involving C++ exception unwinding in
the picture. In fact, it doesn't require any unwinding at all.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
> When an error occurs I generally need to know how we got there and
> that isn't the time to add code to provide context.
Really? I find the context is very rarely interesting except for
debugging purposes. The only time I need to know the context is when
I have a very specialized way to recover from one particular
condition. I can count the number of times that's come up for me on
one hand, and then it is usually dealt with so close to the point of
detection that I wouldn't be throwing an exception in the fist place.
> Some problems are so difficult to reproduce you don't have that
> luxury.
It sounds like the problems you're talking about are bugs.
Distinguishing between bugs and the sorts of things that one should
handle with exceptions is fundamental.
--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com
The Astoria Seminar ==> http://www.astoriaseminar.com
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
Well, occasionally people write about using exceptions at
just the top level and that it is fair to compare that
with return codes. Earlier in the thread Otis Bricker
posted some results that compared the two approaches and
the return code approach was about 10% slower than using
EH. IMO it would be more realistic to add a few layers
to his test and catch exceptions in the middle somewhere
as well as at the highest level. I wouldn't be surprised
if the EH approach was then a little slower, but was
still considered useful because it was a little easier
development wise.
> Consider a routine doing some calculations. As they are rather
> expensive to perform, it might be a good idea to save the result for
> later. So the routine managing the calculations calls a general
> caching routine, which calls a specific storage strategy routine,
> which calls insert on a std::vector, which might call operator new to
> allocate additional space.
>
> If the allocation fails, what should operator new do? It doesn't have
> a clue! Neither has the vector.
>
> The top level routine, on the other hand, might consider throwing out
> some other values to make room, or just drop the current value and
> recalculate it later. It has the proper context to decide.
>
Going all the way to the top level if you don't have to is
going to take more resources in destructing and rebuilding
than catching things at lower levels. The existing, valid context
should be protected by catching exceptions at
multiple levels.
Brian
The connection is as follows. The hardware raises a hardware exception.
The operating system has handlers set up to intercept them (that's the
job of the os) and do something meaningful with them. Under Windows,
what this means is it is transformed into a Structured Exception. SE's
can be intercepted by user code, and the SEH model supports both the
termination model (which is what C++ EH relies on) and the resumptive
model (which is what the gc EH relies on).
Both the gc EH and C++ EH are built on top of the SEH mechanism. The gc
EH is not built on top of C++ EH.
Now, what happened with 64 bit Windows is Microsoft *completely changed*
how SEH works. This affects both gc EH and C++ EH because both are built
on top of SEH. So while gc EH is not reliant on C++ EH, they both share
a common reliance on SEH, so if that underlying implementation is slow
in response, then both gc and C++ EH get slow in response.
> You can do all that stuff without involving C++ exception unwinding in
> the picture. In fact, it doesn't require any unwinding at all.
The way Microsoft Windows provides access to the exceptions is not
something that user level code can change. If your program wants to
intercept hardware exceptions, you MUST play ball with the way the
operating system chooses to present them, and for Windows, that's SEH.
It is also true that a C++ implementation can choose to ignore SEH and
roll their own EH implementation. But the downside of that is the usual
downside of ABI incompatibility - you're in your own world and cannot
talk with binaries compiled with any other compiler or binaries in any
other language. Another downside is when a hardware exception does get
thrown, none of your exception handlers will get executed.
--------------
Walter Bright
http://www.digitalmars.com C, C++, D programming language compilers
http://www.astoriaseminar.com Extraordinary C++
--
You keep talking about saving counters. The assembly
language for this code shows the compiler keeping the
array construction count in registers without doing
anything special to save them in the caller.
> In particular, how were you able to compile
> your code with EH turned Off if you explicitly throw
> and catch exceptions?!
Why would I want to? If you're interested in that, just
comment out the exception parts and use whatever flags
you want.
In particluar, 14.1 "Error Handling" section of TC++PL 3rd starts
with:
"The author of a library can detect runtime errors but does not in general
have any idea what to do about them. The user of a library may know how to
cope with such errors but cannot detect them - or else they would have been
handled in the user's code and not left for the library to find."
That is you don't need EH facilities if you can handle the error
locally.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
Sorry, but this whole problem comes from you are using the wrong API.
If
you're usign SEH, it is not only inefficient, but also incorrect
because
any stack frame might see your exception, which violates the
requirement
on gc that it is invisible to the user, and, moreover, intercept it
and
abort the execution or whatever. You need some API that does not play
with
the stack anyhow. Under UNIX, you would use signals of some sort.
Under
Windows, you'd probably use vectored exceptions, see
http://msdn.microsoft.com/msdnmag/issues/01/09/hood/
Regards
Jiri Palecek
> But by using status codes to report problems, you also constrain the
> compiler to generate less optimal code.
>
Not exact.
...using status codes to report LOCAL problems.
--
With all respect, Sergey. http://ders.stml.net/
mailto : ders at skeptik.net
--
--
--
Thanks for the suggestion, I had never heard of vectored exceptions.
This is no surprise, since they are a new addition in 2001. The gc I
added this to was done years earlier, when vectored exceptions did not
exist.
-------------
Walter Bright
http://www.digitalmars.com C, C++, D programming language compilers
http://www.astoriaseminar.com Extraordinary C++
--