are exceptions expensive on a render engine?

Started by
23 comments, last by SiCrane 11 years, 11 months ago
ok, basically that... I'm starting to build a render engine in DirectX, but I'm not sure how expensive could be to use exceptions to handle some errors.

I'm thinking on doing something like:


HR = SomeD3DFunction( bla bla bla );

if( FAILED( HR ) )
throw SomeDeviceException();



is it safe?.. or would it be too expensive to use?

thanks!
"lots of shoulddas, coulddas, woulddas in the air, thinking about things they shouldda couldda wouldda donne, however all those shoulddas coulddas woulddas ran away when they saw the little did to come"
Advertisement
From a performance perspective, you'd only want to throw an exception on error either if the chances of the error occurring are very low or if program execution is so hosed that performance doesn't matter since you're about to bail (ex: to desktop or the main menu). For MSVC I generally use the estimate of one in ten thousand as the cut off. If the call is more likely to fail than one in ten thousand calls, then don't use an exception. If less, then an exception is fine. So a function like IDirect3DDevice9::TestCooperativeLevel() would be a bad candidate for throwing an exception since a lost device is something that can easily happen and also can be recovered from so you aren't about to bail.

From a performance perspective, you'd only want to throw an exception on error either if the chances of the error occurring are very low or if program execution is so hosed that performance doesn't matter since you're about to bail (ex: to desktop or the main menu). For MSVC I generally use the estimate of one in ten thousand as the cut off. If the call is more likely to fail than one in ten thousand calls, then don't use an exception. If less, then an exception is fine. So a function like IDirect3DDevice9::TestCooperativeLevel() would be a bad candidate for throwing an exception since a lost device is something that can easily happen and also can be recovered from so you aren't about to bail.


I'm not sure if I entirely agree with that. Exceptions are used in situations where behavior is, well, exceptional. You use exceptions when you _want_ to be able to recover from an error. You use assertions in situations where you don't.

Basically, assertions verify that the contracts between functions are being honored; if a function specifies that it cannot take a NULL parameter, yet a NULL parameter is passed to it, you should use an assertion; that is a logic error. If it was data passing the NULL parameter, the caller should have been aware of the contract and verified it beforehand, possibly using a thrown exception to early-out.

If he's working with code where he must roll-back logic due to a manageable error, then an exception is correct. If it's an unrecoverable error, one should use an assertion.

Ideally, with a renderer, you'd rarely if ever use assertions (only use them to validate function contracts); you want your renderer to be able to recover from errors and at least restart itself.

The alternative to using an exception is to check the return value and do a series of returns, which is no more efficient, and is far harder to read/understand.
Assertions are used to flag situations that should never happen, and only arise from the fundamental assumptions of the programmer being violated. This does not mean just any situation that you know can happen but you can't or just don't want to recover from. For example, being unable to open a script file from disk that contains the core logic of your program is not recoverable, but does not warrant an assertion because file system errors, running the program from a network or external drive or plain bad installs are situations that can actually happen. Every assertion that gets tripped should represent a programmer error, and should not get used for any other situation. In other words, if the end user ever sees an assertion get tripped, then a programmer somewhere has screwed up.

The efficiency of using return values compared to exceptions depends on the relative frequency of the failure situation. Throwing and catching an exception is an extremely costly operation, so if an error occurs frequently then, from a performance perspective, exceptions become more costly than using return values. On the other hand, if an error almost never occurs, then it's more efficient to not have to check a return value. The break even point depends on multiple factors including compiler, but on MSVC it usually turns out to be the roughly one in ten thousand value I mentioned.
Be aware that exceptions aren't just something you can add in willy-nilly. Firstly, there's overhead for simply *having* exceptions at all, you don't have to throw an exception to get a performance hit, its there all the time. The overhead is probably negligible in many functions, but for functions which might be called in a tight loop it can be significant.

Another concern is that there's still more overhead, either in implementation, at runtime, or both, to make sure your functions are exception-safe. It's also something that can be difficult to get right. An example is that if you allocate memory inside of a function, and then later the stack is unwound due to an exception, you might not execute the code that frees the memory. If your program is going to bail anyhow, its not big deal, but if you're able to recover from the exception higher up the chain, you've got a source of memory leaks. Proper use of smart pointers can go a long way to solving that particular problem, but its a good example of how using exceptions radically changes the way you have to think about your program's correctness.

On some development platforms portability can also be a concern as well, because exceptions might not be supported. Sticking to PC and (probably) modern consoles it will be well-supported, but you might be out of luck if your code will ever run on an embedded platform or portable consoles. I'm not privy to specifically which development systems might or might not support them, but historically speaking, RTTI and exception handling are usually the first things thrown out the window on less-capable platforms, so its something to keep in mind.

throw table_exception("(? ???)? ? ???");

I tend to be in the group that suggests against using exceptions unless you actually know what you're doing.

Exceptions introduce a lot of extra work YOU have to manage (smart pointers being one simple example). They also tend to add a certain amount of performance overhead (in C++, this is not completely true in managed languages). Why? Well, there's a couple of things to realize: When an exception is thrown the stack has to be unrolled. In C++ this means fully constructed objects whose scope ends during the stack walk up to the first exception handler that matches the thrown exception has to be destructed. For things like smart pointers, containers, etc. this can be quite expensive (when a vector goes out of scope, it has to first walk all the existing items in it and call their destructors, then release the memory it had allocated).

In addition, there aren't a whole lot of areas in a RENDERER where you actually NEED exceptions. There are a few things that MIGHT call for them, such as errors like "Device was removed." (I've gotten that one before :P). However, in general most errors are quite recoverable, such as the device being lost. In C++ the general rule is that "exceptions are for exceptional events," and a device being lost is a very common occurrence (in D3D9). Other similar issues, such as running out of GPU memory when trying to allocate a texture or vertex buffer are similarly not that uncommon and should be dealt with in a fairly clean and failproof mechanism. All of this generally suggests that you should either provide error code returns (instead of exceptions) or some other form of error notification.

As a last note: Never, and I do mean it, mix exceptions with return codes. There is no interface more confusing than one that attempts to report errors in more than a single way.

As an additional note on the managed side of things: The reason exceptions are more prevalent, other than just a language design decision is that it is quite a bit cheaper for exceptions to propagate than it is in a language like C++. This is because, in most managed languages (like C#, Java, Python), there is no need to walk up the stack destroying objects whose lifetime has ended. These will be automatically collected by the GC. This simplifies the stack walk and reduces the amount of additional information the compiler has to track to figure out what needs to be cleaned up.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.


Be aware that exceptions aren't just something you can add in willy-nilly. Firstly, there's overhead for simply *having* exceptions at all, you don't have to throw an exception to get a performance hit, its there all the time. The overhead is probably negligible in many functions, but for functions which might be called in a tight loop it can be significant.

To be fair, checking the return value for functions called in a tight loop can have significant overhead too. In any case, degree and kind for exception overhead is an implementation specific deal. Some compilers add prologue and epilogue code to every function, which obviously causes overhead for every function. On the other hand, some other compilers have what are called "zero overhead exceptions", which is a marketing claim rather than an accurate claim, but the cost takes the form of additional data tables added to the executable rather than specific runtime costs like the prologue/epilogue code. It slows down load time due to increased size, but doesn't actually slow down code execution until an exception is actually thrown. Additionally, compilers frequently give mechanisms to help manage exception handling overhead, such as MSVC's __declspec(nothrow).

They also tend to add a certain amount of performance overhead (in C++, this is not completely true in managed languages). Why? Well, there's a couple of things to realize: When an exception is thrown the stack has to be unrolled. In C++ this means fully constructed objects whose scope ends during the stack walk up to the first exception handler that matches the thrown exception has to be destructed. For things like smart pointers, containers, etc. this can be quite expensive (when a vector goes out of scope, it has to first walk all the existing items in it and call their destructors, then release the memory it had allocated).

That's not really an overhead of exception handling. The compiler still needs to unwind the stack and destroy the same objects no matter what language constructs you use to handle program flow. Use a return to leave a function or use an exception to leave a function, the stack gets unwound either way.

[quote name='Washu' timestamp='1336675070' post='4939079']
They also tend to add a certain amount of performance overhead (in C++, this is not completely true in managed languages). Why? Well, there's a couple of things to realize: When an exception is thrown the stack has to be unrolled. In C++ this means fully constructed objects whose scope ends during the stack walk up to the first exception handler that matches the thrown exception has to be destructed. For things like smart pointers, containers, etc. this can be quite expensive (when a vector goes out of scope, it has to first walk all the existing items in it and call their destructors, then release the memory it had allocated).

That's not really an overhead of exception handling. The compiler still needs to unwind the stack and destroy the same objects no matter what language constructs you use to handle program flow. Use a return to leave a function or use an exception to leave a function, the stack gets unwound either way.
[/quote]
Yes and no. If the performance were the same then there would be no reason to use error codes over exceptions. There is a performance difference though, and it does directly relate to the tracking of what needs to be destroyed when an exception is thrown, either through additional tracking information on the stack or in static tables. All of which is generated by the compiler and thus invisible to the programmer. Although the table based approach (which uses the current call stack to find which destructors to call, I believe) is supposed to be "cost-free" until an exception is thrown.

My only other thing is that I do tend to prefer exceptions over error codes, mainly due to the reduction in spaghetti code that most return based error handling introduces (which it doesn't have to, but it tends to). But that only really works if you've designed your software solution with exceptions in mind.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

Depends on the exception handling mechanism. The old SJLJ "personality" that GCC used to use by default was horribly slow. The newer DWARF "personality" will often be cheaper than checking error codes, as the cost is paid only when exceptions are thrown (which should be rare -- you certainly don't want to replace error codes with exceptions entirely). The Visual C++ exception handling mechanism is somewhere in the middle, as far as I understand it; quite fast but not free.

But I'd say that any such decision should hang far more on the skill and experience of the people writing and maintaining the code than on any concerns about efficiency. Writing exception-safe code requires a meticulous attention to detail and an awareness of which code can/may throw. Example:


struct Contact // for an address book app, or something.
{
Contact(const std::string &name, const std::string &address) :
name(name),
address(address)
{ }

std::string name;
std::string address;
};


The compiler-generated copy-assignment operator isn't strongly exception safe, as if address assignment throws, you'll end up with a corrupted Contact (in the sense of a name/address mismatch). A contrived example, but it shows that even if RAII is applied ruthlessly, you aren't out the woods; you have to be ever vigilant.

The burden is probably greater than checking the return code of every function. But with that said, C++'s built-in handling of stack-unwinding and deterministic destruction arguably provides a better baseline than most any other mainstream languages for handling exceptions (when treated as non-fatal conditions).

This topic is closed to new replies.

Advertisement