GameArchitect.net: Musings On Game Engine Structure and Design

 

Home

What It's All About

Who Is This Guy?

The List

Complete Archive

Personal 

RSS Feed

People




 

 

Exceptions and Error Codes

By Kyle Wilson
Wednesday, May 03, 2006

As software engineers, we are admonished by the doyens of C++ programming that the correct way to report error conditions is to throw exceptions.  As game developers, we are the heirs to a vast body of common wisdom holding that exception handling overhead is too expensive for high-performance games, and that error codes are the only acceptable mechanism for propagating errors in game code.  What should we believe?  Should we write this?

int Function()
{
    if (error)
        return -1;

    return 0;
}
Or this?
void Function()
{
    if (error)
        throw std::exception();
}

And why?

Why Are Exceptions Good?

Throwing an exception is, in many ways, like returning an error value from a function. It returns control of the program to the scope from which a function was called and it indicates that an error occurred during execution of the function. Yet Herb Sutter--C++ guru, chairman of the ISO standards committee, man about town--and co-author Andrei Alexandrescu advise us to "prefer using exceptions over error codes to report errors" [1]. Why do modern software engineering practices encourage us to prefer exceptions? Because exceptions have a number of features which error codes lack:

  • Exceptions can signal errors from within constructors and overloaded operators. With exception handling disabled, the only mechanism for handling operator and constructor failures is the cumbersome one of setting an error flag in the object itself, which must be queried to see whether the object is in a valid state.
  • Exceptions cannot be ignored. Functions that use error codes to indicate failure to their calling scope must be wrapped in FAIL_RETURN macros or some similar mechanism to propagate errors to the scope in which they will be handled. This is unsightly and easy to forget. Exceptions will propagate automatically. Exceptions that are not handled will terminate the program, which is preferable to leaving the program executing in an invalid state.
  • The standard library uses exceptions. The std::string class can throw std::out_of_range. Standard new can throw std::bad_alloc. Calls to dynamic_cast can throw std::bad_cast. All of these standard mechanisms must be evaluated and adapted into other error-handling mechanisms or ignored by programs that run with exception handling disabled.

As I've mentioned in a previous article, I achieved substantial memory savings and modest speed improvement by turning off exception handling in MechAssault 2. But it would be shortsighted to decide that exceptions are a bad idea because one can efficiently disable them in a program that doesn't use them. That program is already paying the branch-miss cost of conditional checks around every function that can return an error code. In theory, a program that uses exceptions to indicate error conditions could be more efficient, if the exception-handling overhead were small enough in the absence of an exception and if exceptions were thrown relatively infrequently.

What Do Exceptions Cost, In Theory?

It's possible for a compiler to implement exception handling in such a manner that there is no performance cost unless an exception is actually thrown [2]. To do so, the compiler generates table data that maps any possible instruction pointer address to a description of objects in the current scope that must be destroyed and to try/catch blocks in the current scope that are active. This is quite a bit of data, though it could be kept contiguous so as not to interfere with cache locality until an exception occurs. Zero-overhead exceptions are expensive when an exception is actually thrown, however, since every function on the unwinding call stack requires a fresh search through the table data for its instruction pointer location.

Neither Microsoft C++ nor GCC implement zero-overhead exception handling. Instead, both compilers add prologue and epilogue instructions to track information about the currently executing scope. This enables faster stack unwinding at the cost of greater runtime overhead when an exception isn't thrown.

As Vishal Kochhar describes in [3], the Microsoft C++ compiler creates hidden local variables in each function that reference an exception handler function and a data table. The data table is similar to the one I described zero-overhead exception handling implementations using, but in Microsoft's implementation there is a small table for each function instead of a single large table for the entire instruction address space.

In Microsoft C++, the overhead of exception handling in the absence of an exception is the cost of registering an exception handler as each function is entered and unregistering it as the function is exited. As far as I can tell, that's three extra push instructions and two movs in the prologue, and three more movs in the epilogue. Cache locality is also negatively affected by exception handling overhead, as exception handlers are now scattered throughout the instruction space.

The overhead of exception handling when an exception is thrown is the cost of unwinding the stack, iterating over the function info tables for each function. Each function's data table must be searched to find stack objects requiring destruction and to check for type-appropriate catch blocks for the exception that was thrown. Even with the additional bookkeeping that Microsoft's implementation does at runtime, this is likely to be very slow.

What Do Exceptions Cost, In Practice?

To measure the true cost of exceptions, one should compare them to an alternative mechanism for indicating errors. For my tests, I wrote two functions, each of which fails with an error some small fraction of the time:

int ErrorCodeFunction(int count, int errorMod)
{
    EmptyClass emptyObj;

    if (count % errorMod == 0)
    {
        return -1;
    }

    return 0;
}

void ExceptionFunction(int count, int errorMod)
{
    EmptyClass emptyObj;

    if (count % errorMod == 0)
    {
        throw std::exception();
    }
}

Each function contains a local variable of an empty class type, the constructor and destructor for which are defined in a separate translation unit. This prevents the compiler from eliding the usual exception-handling prologue and epilogue when stack-unwinding is enabled. I called each function from a loop of the form:

for (int I = 1; I < iterations; ++i)
{ERROR_FAIL_RETURN( ErrorCodeFunction(i, errorMod) ); 
}

In my actual test code, I unrolled the loop sixty-four times so that my tests wouldn't be dominated by loop iteration overhead. I #defined variants of the ERROR_FAIL_RETURN macro so that the tests for ErrorCodeFunction would return any failing (non-zero) error code returned. The tests for ExceptionFunction simply called through to the function; any exceptions thrown propagate up the stack automatically.

The interesting numbers to compare here are the performance of ErrorCodeFunction with stack unwinding disabled and of ExceptionFunction with stack unwinding enabled. I give timings below for my home PC. My computer is a 3.2 GHz Pentium D with three gigs of RAM. My tests were compiled with Microsoft C++ v. 8.0. Relative results on the Xbox 360, also using Microsoft's compiler, were similar. In my initial tests, I made "errorMod" greater than the number of iterations, so that no error was ever actually returned and no exception was ever actually thrown.

The results of this test bear out the conventional wisdom of game development. Sixty-four million iterations of the error code function with stack unwinding disabled take 1.411 seconds. The same number of iterations of the exception-throwing function with stack-unwinding enabled (compiler option /EHsc) take 1.525 seconds. Exception handling is substantially more expensive than error codes.

With stack-unwinding enabled, the error code function was even more expensive, taking 1.600 seconds. If you compile with support for exceptions, you pay the cost whether you use them or not--so you might as well use them.

As well as being curious about the overhead of error-checking in error-free code, I was also curious about the costs of actually returning errors and throwing exceptions. So I modified my unrolled timing loop to continue to the next iteration if a call to an error code function failed and wrapped the exception version in a try block with an empty catch. Results were:

Continue 1/1000000 Error Code Function1.59981977 s
Continue 1/1000000 Exception Function1.53195719 s
Continue 1/100000 Error Code Function1.59838406 s
Continue 1/100000 Exception Function1.53634095 s
Continue 1/10000 Error Code Function1.59790273 s
Continue 1/10000 Exception Function1.57429491 s
Continue 1/1000 Error Code Function1.55498578 s
Continue 1/1000 Exception Function1.96710075 s

The fractions in the test names are the frequency with which errors are returned. As can be seen, exceptions become more efficient than error codes when the error frequency is between one in one thousand and one in ten thousand.

Best Practices

The conventional wisdom of game development holds true. If you're writing a performance-critical game, you should compile with stack unwinding disabled and not use exceptions in your code. You should use error codes where possible. You should factor out possible constructor failures using the construct/initialize idiom. If you write overloaded operators that can fail, you'll have to indicate failure by setting flags on the object being operated on.

If you're writing minesweeper, though, or writing tools in which realtime performance isn't so essential, exceptions should still be preferred for their greater flexibility or functionality. Should they be preferred to error codes in all situations? In their article on the subject entitled "Graceful Exits" [4], Jim Hyslop and Herb Sutter suggest that exceptions are appropriate for serious errors, because they cannot be ignored, but that less critical errors could appropriately be signaled by error codes. I'd argue that exceptions are appropriate for truly exceptional circumstances; based on the profiling results above, they should be preferred for efficiency reasons if an error will occur 0.01% of the time or less. That class of events comprises truly unusual behavior, like disk failures to read or other hardware failures. Exceptions may also signal constructor or operator failures, though in general it's best to avoid situations where these could occur in the first place.

Finally, no comment on error-handling best practices would be complete without mention of exception safety. An exception-safe program is one which remains in a valid state if an exception is thrown. When an exception occurs, such a program will leak no resources and voilate no class invariants. Exception safety is facilitated by a variety of idioms: resource acquisition is initialization (RAII), the PIMPL idiom, smart pointers, and nothrow-guaranteed swaps.

The key point that I want to make here is that exception safety isn't just something to be concerned about if you actually use exceptions and compile with stack-unwinding support. Whether you throw an exception or return an error code, whether you're calling a function that throws an exception or you're wrapping a function call in a FAIL_RETURN macro, you should write functions that can safely exit at any point. Even if you think you know all the return points in your code and have done all necessary clean-ups before each, someone else is going to have to modify your function someday, and there's no reason to leave him or her a brittle function that's easy to break.

Even in performance-critical game code compiled without exception support, prefer to use the idioms of exception safety except where they incur measurable costs. Use smart pointers and RAII, and make sure that the functions you write are safe to exit at any point, and you'll make your entire engine more stable and robust for your co-workers and for your future self.

References

[1] Herb Sutter and Andrei Alexandrescu, C++ Coding Standards. Addison-Wesley, 2005.

[2] Drew, S., Gouph, K. J., and Ledermann, J. "Implementing Zero Overhead Exception Handling." Tech. Rep. Technical
Report 95-12, Faculty of Information Technology, Queensland University of Technology, 1995.

[3] Vishal Kochhar, "How a C++ compiler implements exception handling." Posted 16 April, 2002 at www.codeproject.com.

[4] Jim Hyslop and Herb Sutter, "Graceful Exits", C/C++ User's Journal, June 17, 2005.

Any opinions expressed herein are in no way representative of those of my employers.


Home