Rants on software, computing, and any other topic I feel like.

Wednesday, November 24, 2010

Exceptions are Faster

There's a lot of C++ hating around these days. This hate doesn't always come from the world of young bucks in love with Python. Seasoned C programmers like to hate on C++ for it's nice features. Because, as all good purtitans know, anything that feels good or makes life easier must be bad.

One particular C++ language feature that is generally avoided are exceptions. Even C++ programmers, who love exceptions for their ability to clean up ugly error handling code, are guilty of this. The general consensus seems to be that exceptions should only be used in parts of code that aren't time sensitive. Cache invalidation and all sorts of other badness are usually the reasons given that exceptions are slow.

The alternative in that situation is to use return codes. Yeah, sure they're ugly and use up that nice return value slot that could otherwise be used for something nice, but nothing is sacred when performance is required.

Well, I was thinking about that today and decided that this is all wrong.

The logic is this: In order to handle return codes properly, you must check them all the time. When you're writing time sensitive code, checking an error code that isn't going to be set 99.9% of the time is a big fast waste of time. In fact, looking at any flag that isn't set 99.9% of the time is really stupid. There's usually a better way to handle the logic. Spending the same amount of time for rare cases and common cases doesn't make sense. It's okay for the code that handles rare cases to be slow as long as the common case is as fast as possible. The overall speed won't be significantly affected by the rare case.

This is why exceptions are so great. With exceptions, you don't actually check anything most of the time. In other words, the common case of no error, takes no time at all. The compiler takes care of figuring out where exceptions should be caught at the points in the code where they're thrown. The way it does that is to simply "goto" the catch point. When using an error code, the calling code is checking for the error code every time. Yeah, it's slow because it's jumping all over the place. But so what? It only happens on rare occasions, which can be slow. If your code is designed such that errors are relatively rare occurances (as it should) then there is likely very little impact. Of course, if you're using exceptions for program logic, then well, you're doing it wrong.

All right, here's the proof that exceptions are faster than return codes. I created simple class that increments a value until it hits some maximum value. It is considered an error when it hits that maximum value. The return value version of this simply returns false when an error occurs, while the exception version throws an exception.

The return value version:

class inc
{
public:
inc(int m) : m_max(m), m_count(0) {}
bool go(int& x)
{
if (m_count > m_max)
return false;
x = ++m_count;
return true;
}
private:
int m_count, m_max;
};

int main()
{
int count = 1000000000;
inc f(count);
int x;
while (1)
{
if (!f.go(x))
{
printf("error\n");
return 1;
}
}
}

The exception version:

struct my_exception : std::exception
{
virtual const char* what() const throw () { return "error"; }
};

class inc
{
public:
inc(int m) : m_max(m), m_count(0) {}
int go()
{
if (m_count > m_max)
throw my_exception();
return ++m_count;
}
private:
int m_count, m_max;
};

int main(int argc,char** argv)
{
int count = 1000000000;
try
{
inc f(count);
int x;
while (1)
x = f.go();
}
catch (std::exception& e)
{
printf("%s\n", e.what());
return 1;
}
}

Before we cut to what you're waiting for, the benchmark, let's look at a couple things here.

First, the exception version is more self-documenting. When the error occurs, it's clear what the error is. It also catches more errors than just the one I'm aware of. Any other standard exceptions will be caught and the program will then exit gracefully with a possibly helpful error message. To accomplish this with the return code version, we would have to setup a error code system with associated enums, functions, etc. With this system, we just hook into the already created std::exception class.

Second, the code for the exception version is more elegant. Take the signture for the "go" function for example. In the return value version, I'm already using the return value for an error code, so I have to return the value by parameter reference, which isn't the natural way you'd want to do this. The exception version does the natural thing and just returns the value.

Now, the benchmark. I didn't do lots of testing with lots of compilers and so forth. Feel free to do so and torture me mercilessly, but I have my doubts that you'll get widely different results. On my machine here at work, an i7, the above code runs in about 4.8 seconds when using return values and 3.6 seconds when using exceptions, a 33% improvement.

Labels: ,

Comments:
Don't hold your breath on this one.

With VC++ 10 and optimizations enabled, they take basically the same time. But the compiler inlines the function and in both cases the generated code is the moral equivalent of a for loop followed by an error, with the "C++" variant having somewhat more awkward code and a few things for the exception object.
 
Too funny. It took seconds on an i7? Either something's wrong with your calculations or you should get your money back from Intel. Also, there's no calls to clock() in the code. How are you timing it? At least go through some iterations and average; e.g.:

int main(void)
{
const int NUM_ITERATIONS(32);
clock_t start, end;
int sum(0);
for (int i = 0; i < NUM_ITERATIONS; ++i)
{
int x;
int count(1000000000);
inc f(count);
start = clock();
try
{
while (1)
x = f.go();
}
catch (std::exception& e)
{
}
end = clock();
sum += end - start;
}
printf("%f", (double)sum / (double)NUM_ITERATIONS);
}
 
@GrayShade It's certainly not slower as many might argue. Exceptions when used normally shouldn't be any slower than a return value based system. I must admit that the code I posted is simplified from the code I used to test. My more complex code makes an effort to keep the compiler from making optimizations on the simple code that wouldn't be possible on more real world code.

@Joe T. As above, the code I posted isn't quite the same. As for timing, I ran a number of iterations using the build time command of bash and it was consistent so I had faith in that.

The code I originally had as sample did far more in each iteration having nothing to do with error codes. That code read characters from a large file. While the timing difference was much smaller between the two systems, it was still measurable and significant, something like 4% instead of 33%.

The point is that you shouldn't dismiss exceptions in the design process on performance grounds.
 
I have tried the code with g++ on Ubuntu 10.04 on a Intel Dual Core 2 calculating time spent through bash time program.

The result shows an improvement of about 10% in version that use exception.

Interesting article.
 
Post a Comment

<< Home

This page is powered by Blogger. Isn't yours?