[blog] C++ exceptions can be an optimization
#1
One of the first things I sought to do when I first started contributing to PCSX2 was to improve the emulator's overall stability and error handling; and to this day it's still one of my top priorities.

My method of doing so was initially seen as controversial: I merged in drk||Raziel's VTLB code (which was C++), converted the rest of the PCSX2 codebase to C++, and started replacing the (lack of?) error code return values with modern C++ exception handling. The initial reaction from the public (and some PCSX2 team members) was either distrust or panic. Chants of "C++ is slow!" or "Exception handling is slow!" frequented the PCSX2 revision comments.

And admittedly, for some tasks and in some specific scenarios, C++ and it's exception handling are slow. But, of course, the key is to avoid those scenarios... which as it turns out is really quite easy. Better yet, clever use of C++ and its exceptions can actually be a speedup. How is that possible? I'll explain!

Typically in traditional error handling models, you check the return code of a function for errors, like so:

Code:
if( DoSomethingSpecial() == SPECIAL_FAIL )
{
    // Handle error.
}

This is simple, short, and quite fast compared to the overhead of entering a C++ try/catch block. But let's consider a more practical everyday example:

Code:
int DoSomethingSpecial()
{
    if( DoSomethingElse() == SPECIAL_FAIL ) return SPECIAL_FAIL;

    // Do stuff based on DoSomethingElse's success
    Console::WriteLn( "Success!" );

    return SPECIAL_WIN;
}

void LoopOfCode()
{
    do
    {
        // code [...]
    } while( DoSomethingSpecial() != SPECIAL_FAIL )
}

The above code snippet must perform no less than two conditional checks per loop just to propagate the error code up the chain of function calls, and this isn't even handling the possibility of a function returning more than one error code yet! This is a situation where C++ Exception Handling can come to our rescue:

Code:
void DoSomethingSpecial()
{
    DoSomethingElse();

    // Do stuff based on DoSomethingElse's success
    Console::WriteLn( "Success!" );
}

void LoopOfCode()
{
    try
    {
        while( true )
        {
            DoSomethingSpecial();
        };
    } catch( Exception::SpecialFail& )
    {
    }
}

The above C++ snippet performs the exact same operation, except now no conditionals are needed. We've traded off the two conditionals per loop for the entry/exit code for the try/catch block. But the block is outside the loop, so it will be run only once. Conditional checks are one of the slower operations on almost any CPU design, which means if the loop is a busy one which spins frequently this C++ code will certainly be a significant speedup over the plain jane C version. And that's just with one return code. Adding multiple exception handlers doesn't impact performance at all, so in a case where there are multiple return codes the C++ exception handling approach shines even brighter.

... thus dies the age-old rumor that C++ is slower than C. IT's all in how you wield your sword. Or... well... programming language.

Edit: I should add that the basic theory of optimization I'm using here is what I call "optimizing for the common case." It's a process of speeding up the code that's being run more frequently (which in our example above is a typically error-free running loop) by offloading the logic to an area of the code that's run much less frequently (the exception handler's entry/exit overhead). It's one of the most powerful optimization techniques any programmer can employ.
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply

Sponsored links

#2
You right, at modern ages exception handling is pretty fast, and you could get speedup even on usual if / else block with unbalanced true/false path. But you should not forget about code readability: in you example catch statement and Do function has no direct connection, so if there is a few lines between them you COULD made whole code part unclean!

p.S. C-style code for such case are pretty funny -- why do you chose such style? You should be use longjump here, and while(true) cycle. You code is not real C, rather Pascal-style.
Reply
#3
Admittedly I have never seen setjmp/longjmp used in live C code. I've never considered it a viable solution to anything in a modern programming sense. Although since you brought it up I'm thinking I might use it to replace the custom crap coroutine implementation in IPU -- maybe it'll solve the random optimization failures since it has proper read/write boundaries. Butthen again I hardly consider IPU's design sensible or modern. It'd be better off using pthreads and the operating system's built in task switcher.

Furthermore I believe the C++ version to be more readable than the C version, since it's less code and a linear execution pattern. And it's most unquestionably more sensible than a C version using setjmp/longjmp would be. I mean sure, setjmp/longjump isn't exactly complicated but it does tend to result in a bizarre code arrangement, and verbose switch statements if handling more than one exception/return code. So to fault C++ on readability and then recommend setjmp is, as you say, pretty funny.
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply
#4
Nice blog post.
I personally find the non-exception handling code easier to read however, since it just makes more sense with the thinking "You run a function, the function returns a value, you do something with the result of the value."

The Exception handling code just makes you focus on the "succeeded part" with the actual handling of the exception else-where.

I understand it can be a good speedup on critical areas, but for non-critical code, it's more readable/simpler to go with return codes IMO.

And personally I think the Try/Catch blocks just look ugly in code.
Check out my blog: Trashcan of Code
Reply
#5
The trouble with proper error handling is that it's never pretty. So it's usually taking the lesser of many evils, which really does end up being C++. I honestly can't even think of any C code that had decent error handling in it, and if there were you would not be pleased with the results. I've never even seen DirectX source code, for example, that actually bothers to do error checking on every function call, because it's just horribly tedious to have 8 to 16 separate if() else clauses on each individual API call, with individual gotos for each stage of cleanup. It turns what could be 24 lines of code in C++ into 320 lines of nightmarish code that could potentially fail to do proper cleanup if you simply shift one pair of calls around. So instead programmers just coded with the "if it fails then a crash is as good a response as any" philosophy.

Using C-style return codes also means things get ugly if you need a function that returns a value to be able to error out (and the function could return 0 or -1 as legitimate results). So you end up in those cases having to pass the out var by reference as a parameter, and then check the return code for validity.

I had added "ok" error handling in Mikmod way back in the day, and it relied on a sophisticated system of function pointer callbacks, error code checking, and entirely thread-unsafe global variables for returning extended error information (similar to the old Win32 GetLastError mess). And I wasn't even bothering to worry about memory cleanup back then Tongue2
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply
#6
... so when you consider that C++ exceptions solve all of those problems in a thread-safe matter, typically generate more efficient code as a result, and all with syntax that's no more verbose or cumbersome than a single if() {} else {} block (yes, really -- it's just something you're not used to seeing, so it looks unnatural), it's really pretty easy to see why they're good.

(also, for the record, a lot of C++ code out there fails to use exceptions properly -- some in fact still put try{} catch(){} blocks around every individual statement as if they were just doing if() blocks. This is fail. There are usually much simpler ways to design the code so that you only need at most a single try block for the entirety of any one function)
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply
#7
I really liked this, thanks Smile
Reply
#8
Hi
A high performance implementation of C++ exception handling is crucial, because exception handling overhead is distributed across all code. The commonly-used table-driven approach to implementing exception handling can be augmented by an optimization that seeks to identify functions for which (contrary to first appearance) no exception handling tables need be generated at all. This optimization produces modest but useful gains on some existing C++ code, but produces very significant size and speed gains on code that uses empty exception specifications, avoiding otherwise serious performance losses.
Reply
#9
Hi
the real problem with C++ exception handling is that it introduces a memory leak threat. Indeed if you allocate some memory in try block you should always free it in the catch block.
And the only practical solution that comes into my mind is smart pointers. Would you please expand on this point?
Reply
#10
Yes, using "smart" pointers is by far the best option when using exceptions. One of the inherent advantages of exceptions as an error handling facility (as opposed to being used as a flow control technique as described in my blog) is that they allow you to have a single section of code handle errors that could have arisen from almost anywhere in a nested chain of commands and events. If you're not using object/smart pointer cleanup techniques then the only way to handle cleanup manually would typically require a massive amount of if( var==NULL ) type checks in the destructor... and also may require individual catch{ cleanup(); throw;} handlers for other subordinate components that have temp allocations that a parent handler would have no context for. That ends up defeating the purpose; you now have just as much error handling code (or more) then you would by using typical C-style flow controlled error handling.

Scoped Pointers fix most of that.

And really the memory leak problem is inherent to code in C or C++. I've seen lots of C code in my time that fails to do proper cleanup of a complicated initialization procedure if it fails some point in the middle. I've seen other code handle this by implementing a special switch( initState ) handler that it jumps to via goto (with the initState being incremented at each local dynamic memory/device allocation) -- and then still fail because the initState increment is out of sync from the switch handler by one stage, due to a simple foopah on the part of the programmer.
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply




Users browsing this thread: 1 Guest(s)