Some shortcuts :
This document aims to provide data on the cost of exceptions. It has been written to help discussions in SG14, and as such is written in English; be warned though that most links therein lead to my personal site, which is in French as this is the language I use when I teach most of the time.
TODO list :
This might take a few weeks, so be patient; I'll post updates on the mailing list when I'll get around to doing them.
What is the relative cost of exceptions compared to returning and validating error codes? What follows makes the following assumptions:
For these tests, my work hypothesis were:
I hoped to measure the costs of exception handling and error handling in situations which should favor one and situations that should favor the other.
I used 64 bit binairies on a 64 bit machine, as SG14 members have expressed the opinion that this is the most relevant use case
There is an aesthetical aspect to the debate: exceptions do not deface a function's interface the way error handling does, for example; also, the non-local aspect of exception handling mechanisms avoid cluttering client code with error management code. These tests do not explore these issues
Using Exceptions | Using Error Codes | |
In both cases, the test data will be generated in the same fashion. Generation of test data is not included in the timing measurement below:
|
|
|
We use similar functions for the exception-based tests and the error-baseds tests :
Both functions perform the exact same processing in the case where n is valid. |
|
|
The test named check_every_iteration(v) is meant to provide a worst-case scenario for exceptions. It behaves as follows:
Both functions return a pair containing the time elapsed and the error count. |
|
|
The test named check_general_early_exit(v) is meant to provide a close-to-worst-case scenario for exceptions. It behaves as follows:
Both functions return a pair containing the time elapsed and the element at which the first problem has been detected. |
|
|
The test named check_general_late_exit(v) is meant to provide a case scenario suited for exceptions. It behaves as follows:
Both functions return a pair containing the time elapsed and the element at which the first problem has been detected. |
|
|
The test named check_general_no_exit(v) is meant to provide a close-to-ideal (see below for a discussion of this) case scenario suited for exceptions, as code always uses what is presumed to be the « fast path ». It behaves as follows:
Both functions return a pair containing the time elapsed and the element at which the first problem has been detected. |
|
|
Both main() functions are identical, and run the same tests (except for the differences mentioned above) on the same data. |
|
|
I ran the tests on two platforms. The output visible on the right comes from code generated with MSVC 2015, Community Edition, Release, 64 bits. I used my personal laptop for these tests (Windows 7, 2.6 GHz, 8GB of RAM). We can see that in this case, even in the scenarios most favorable to exceptions, error handling was faster. |
|
|
The output on the right in this case comes from the Coliru online compiler. The code was compiled with the following options :
This test shows that in the case most favorable to exceptions (the one where they are effectively exceptional), the exception-handling code is faster than the error-handling one. |
|
|
Some remarks:
What would be the ideal case for exceptions? Simply : no try or catch blocks in the client code, unless there really is a need to (a) perform some cleanup code, e.g. in standard generic functions that are not expected to leave the program in an undefined state, or (b) in containers that have invariants to maintain. These algorithms and containers should then be exception-neutral and in general, once their cleanup stages have completed, let the exception escape where it should. We write code as if nothing bad happened. Should something occur indeed, if we handle it, it's because we care, otherwise, we crash due to an unhandled exception or to a call to std::terminate().
When a deeply nested function faces a problem, it is sometimes necessary to unwind the stack up to the point, either one or many layers above, where the problem can be handled.
The following test uses a recursive function in which non-trivial local variables are instantiated, and provokes a problem situation at various depths down the call stack. In the case where an exception is raised, stack unwinding occurs at each level until someone catches the exception or the program crashes; in the case where an error is returned, said error needs to be treated at each level, and local variables still need to be destroyed as the return statements bring control back up the stack.
The test code I used follows.
For these tests, my work hypothesis were:
Using Exceptions | Using Error Codes | |
The code relies strictly on standard classes and algorithms. |
|
|
Function test(gen,n,m,limit) creates a vector of n elements all initialized with gen(). Then, it computes the sum of the sizes of these string instances and performs Variable rest is used here to ensure that the evaluation order of functions test() and accumulate() does not alter our metrics. |
|
|
Both programs essentially do the same thing, but with exception handling on the left and with error handling on the right. |
|
|
I ran the tests on two platforms. The output visible on the right comes from code generated with MSVC 2015, Community Edition, Release, 64 bits. I used my personal laptop for these tests (Windows 7, 2.6 GHz, 8GB of RAM). We can see that in this case, the code with exceptions is slower than the code with errors by a factor of more than two. This is more than most would have expected. |
|
|
The output on the right in this case comes from the Coliru online compiler. The code was compiled with the following options :
Note that since I used an online compiler, execution runs on a limited budget, which was exceeded in the exceptions case. In this case, the exceptions case is significantly slower... |
|
|
I tried the exact same code but with a longer string, "I love my teacher because he's very cool", in order to avoid the Small String Optimization (SSO). Thus, the only change I made was:
Before | After |
---|---|
|
|
Let's see what the impact of this change is.
For these tests, my work hypothesis were:
I ran the tests on two platforms. The output visible on the right comes from code generated with MSVC 2015, Community Edition, Release, 64 bits. I used my personal laptop for these tests (Windows 7, 2.6 GHz, 8GB of RAM). In this case, the code with exceptions is faster than the code with errors. The main difference between this test and the previous one is that this test relies on dynamic memory allocation within each string whereas the previous one did not. |
|
|
The output on the right in this case comes from the Coliru online compiler. The code was compiled with the following options :
Note that since I used an online compiler, execution runs on a limited budget, which was exceeded in the exceptions case. In this case again, the exceptions case is significantly slower. Could this be due to the quality on the string implementation? Hard to judge from this viewpoint. |
|
|
There is a risk that the results of the previous test could be influenced by the implementation of std::string. To verify this, I used a slightly different version with std::vector<char> instead.
For these tests, my work hypothesis were:
Using Exceptions | Using Error Codes | |
The code relies strictly on standard classes and algorithms. |
|
|
Function test(gen,n,m,limit) creates a vector of n elements all initialized with gen(). Then, it computes the sum of the sizes of these string instances and performs Variable rest is used here to ensure that the evaluation order of functions test() and accumulate() does not alter our metrics. |
|
|
Both programs essentially do the same thing, but with exception handling on the left and with error handling on the right. |
|
|
I ran the tests on two platforms. The output visible on the right comes from code generated with MSVC 2015, Community Edition, Release, 64 bits. I used my personal laptop for these tests (Windows 7, 2.6 GHz, 8GB of RAM). We can see that in this case, the code with exceptions is slower than the code with errors by a factor of approximately 1.5. This is more than most would have expected. |
|
|
The output on the right in this case comes from the Coliru online compiler. The code was compiled with the following options :
In this case, the exceptions case is significantly slower... |
|
|
I cannot compare the impact of exception-handling code on binary size from compiler to compiler right now (I used to have gcc and MSVC on my laptop but had a hard drive crash last week and have not gone around to reinstalling gcc yet; it's a matter of time). I can however compare its impact on the binary sizes with MSVC 2015. Thus, for the test sets we have on this page, we have :
Test Set | Binary Size with Exception Handling | Binary Size with Error Handling |
---|---|---|
Problem checked at various stages |
bytes |
bytes |
Recursive function, problem detected at various depths, with SSO |
bytes |
bytes |
Recursive function, problem detected at various depths, without SSO |
bytes |
bytes |
Recursive function, problem detected at various depths, vector<char> |
bytes |
bytes |
Using SSO or not has an impact on the programs runtime memory footprint, but not on its binary size. This was to be expected.
Let's take a more visual look at the data we have.
In the stack unwinding case with SSO-enabled string instances, we have (lower is better, vertical axis is in mocroseconds, horizontal axis is function call depth):
For this test, g++ with error handling is much better that g++ with exception handling. With MSVC, error handling is also faster than exception handling, but by a much smaller margin.
In the stack unwinding case with non-SSO-enabled string instances, we have (lower is better, vertical axis is in mocroseconds, horizontal axis is function call depth):
This case shows that the situation with g++ does not really change much, whereas code generated MSVC quite consistently performs better with exception handling than with error handling, except for very small depths (in which case the overhead on installing the initial try...catch block probably still shows).
In the stack unwinding case with vector<char> instances, we have (lower is better, vertical axis is in mocroseconds, horizontal axis is function call depth):
The pattern with vector<char> is very similar to the pattern with vector<string> given SSO-enabled string instances.
As far as executable binary size goes, we have (lower is better):
There is a difference in binary executable size. Legend has it that the difference is on the order of 10%, but that is not what my (small) tests seem to show. Indeed, here, the cost in binary size varies between 1.69% and 2.38%.
In summary:
You can use the code above and play with it. Should you find faults with the methodology or the test code, or should you find the time to run tests on other platforms, I would like to know what you get and if it diverges from what my personal experimentations seem to show.