Thread Rating:
  • 2 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[blog] C++ exceptions can be an optimization
#11
I don't know how much helpful it'll be to you but take a look at the Boost exception library . It has some very useful futures as transporting arbitrary data with exception. Boost Unit Testing is also very good. I consider including boost libraries in almost any C++ project as a very good practice. Most libraries in Boost are header-only, so the don't introduce new dependencies. Good Luck Smile
Reply

Sponsored links

#12
You can understand that stuff?

More power to you. I stay as far away from Boost as possible, both for myself and for the sanity of my fellow contributors. Sad

Also, Boost is a good way to increase PCSX2 compilation time to 3x what it is already.

.. and the non-bleeding edge releases of Boost have tons of thread safety issues (most are fixed if you checkout from SVN, but the SVN builds typically won't work in VS 2008 without modifications, and have horrendous dependency issues where various APIs need windows.h included into everything).

Sorry. I'm suffering from a case of "been there and done that." : Packaging custom hack-ups of boost SVNs for the other programmers that were in excess of 26 megs so that I could get proper threading support, and using FTP because it was too slow to use our SVN for it. ... and then still having behavioral differences between VS 2005 and VS2008, because Boost is developed for GCC first and then ported to MSVC later, so the SVNs are rarely cross-compiler friendly. Writing C++ code that no sane person could comprehend without a half dozen years of C++ experience. Trying to just use some basic Boost library sets and being forced to get more and more involved because of constant inter-dependencies. Using code that Boost says "is fast!" and finding out it's still holy-sh** slow after all. Watching our .exe balloon by megs and megs for reasons of templated code bloat, just so we could have the "benefit" of terse syntax that was incomprehensible (but very type safe, which was good seeing how we could barely understand it anymore). It was just an endless nightmare.


(and for the record, I stole a couple ideas from Boost's Exception code for packaging detailed exception data -- but I did it in a much simpler fashion. Oh sure *my* exceptions have a "chance" of failing or leaking a string object in some specific unrecoverable error scenaros ... but do I really care? They're eff'ing unrecoverable anyway!)
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply
#13
A decent article, but I had just a few things I would like to know more about if you don't mind. You said in your post that "Conditional checks are one of the slower operations on almost any CPU design". To perform a conditional check you need merely check whether or not all the bits in two given bit sequences are equal. As far as I know most modern CPUs use a simple XNOR based approach that scales quite nicely and is extremely fast. Just wanted to double check and see if they were really slow, as I'm not very familiar with many facets of modern CPU designs. Finally, as the Google code style guidelines nicely illustrate, exceptions are not quite as nice as they can first appear to be. Just curious if you really understood the massive amount of work you're undertaking by integrating exceptions into such a complex code base. The following are the pros and cons list which caused Google to decide never to use exceptions in their C++ code, worth thinking about before integrating exceptions into a complex code base (taken from http://google-styleguide.googlecode.com/...Exceptions )

Pros:

* Exceptions allow higher levels of an application to decide how to handle "can't happen" failures in deeply nested functions, without the obscuring and error-prone bookkeeping of error codes.
* Exceptions are used by most other modern languages. Using them in C++ would make it more consistent with Python, Java, and the C++ that others are familiar with.
* Some third-party C++ libraries use exceptions, and turning them off internally makes it harder to integrate with those libraries.
* Exceptions are the only way for a constructor to fail. We can simulate this with a factory function or an Init() method, but these require heap allocation or a new "invalid" state, respectively.
* Exceptions are really handy in testing frameworks.

Cons:

* When you add a throw statement to an existing function, you must examine all of its transitive callers. Either they must make at least the basic exception safety guarantee, or they must never catch the exception and be happy with the program terminating as a result. For instance, if f() calls g() calls h(), and h throws an exception that f catches, g has to be careful or it may not clean up properly.
* More generally, exceptions make the control flow of programs difficult to evaluate by looking at code: functions may return in places you don't expect. This results maintainability and debugging difficulties. You can minimize this cost via some rules on how and where exceptions can be used, but at the cost of more that a developer needs to know and understand.
* Exception safety requires both RAII and different coding practices. Lots of supporting machinery is needed to make writing correct exception-safe code easy. Further, to avoid requiring readers to understand the entire call graph, exception-safe code must isolate logic that writes to persistent state into a "commit" phase. This will have both benefits and costs (perhaps where you're forced to obfuscate code to isolate the commit). Allowing exceptions would force us to always pay those costs even when they're not worth it.
* Turning on exceptions adds data to each binary produced, increasing compile time (probably slightly) and possibly increasing address space pressure.
* The availability of exceptions may encourage developers to throw them when they are not appropriate or recover from them when it's not safe to do so. For example, invalid user input should not cause exceptions to be thrown. We would need to make the style guide even longer to document these restrictions!

PS: Just wanted to say that I've been enjoying the blogs and as I was reading through them I thought I might contribute a little input on this one from some fairly reliable sources. Experience has taught me and many others that exceptions aren't worth the effort in existing exception-free code.
Reply
#14
(11-14-2009, 05:10 AM)zenogais Wrote: A decent article, but I had just a few things I would like to know more about if you don't mind. You said in your post that "Conditional checks are one of the slower operations on almost any CPU design". To perform a conditional check you need merely check whether or not all the bits in two given bit sequences are equal. As far as I know most modern CPUs use a simple XNOR based approach that scales quite nicely and is extremely fast. Just wanted to double check and see if they were really slow, as I'm not very familiar with many facets of modern CPU designs.


The slow part is the branching instruction that is typically paired with any conditional check. In rare cases a conditional's flags can be used directly without branching, although that is certainly the exception, and not a rule (and some CPUs don't have any flags at all, eg MIPS).

On the P4 a typical branch instruction was some 20 cycles on average, compared to 1-3 cycles for just about everything else. Best case was 2-3 cycles, worst case 59 cycles. Most AMDs have a 2-3 cycle branch prediction hit result, and a 17-23 cycle miss, compared to 0.5-2 cycles for just about everything else. Core2's are better with roughly 2 cycles for hits and 11 cycles for misses, although they also have an extremely high number of instructions in the 0.33 to 1 cycle count. So any way you slice it, branches are much slower than most other commonly used instructions.

Furthermore, the performance of branching hinges heavily on the Branch Prediction Cache built into any modern CPU. The cache is of limited size, so the more branches you have for a repetitive section of code, the slower your average jump instruction will become for that code. Thus the speed it compounds on itself.

Yeah I'm aware of all the drawbacks of exceptions. In general I find the excuses against exceptions to be right on one sense, but mostly irrelevant in the grand scheme. The code cleanup one is a C programmer's fear, and is totally meaningless to a proper C++ programmer who uses objects for cleanup (same thing any decent C# programmer *must* do always, for example, although it's possible that Google hates C# too). It's an old way of thinking. Furthermore, tons of code out there doesn't do proper cleanup anyway, when encountering errors. The implicit (non-)cleanup of exceptions hardly makes it worse.

Quote:* More generally, exceptions make the control flow of programs difficult to evaluate by looking at code: functions may return in places you don't expect. This results maintainability and debugging difficulties. You can minimize this cost via some rules on how and where exceptions can be used, but at the cost of more that a developer needs to know and understand.
Omg. Flow control evaluation is a ridiculous claim. In modern threaded applications, which must rely heavily on messages and callback events, flow control is effing hard, period. Even if exceptions made it worse (and they absolutely do not), they would be the least of the programmer's worries. The flow control for the PCSX2 gui is impenetrable right now because it A. manages a couple threads, and B. it needs to synchronize a lot of actions and dependencies between them. In order to do so without risk of rendering the gui completely unresponsive to the user for long periods of time (or forever, if a thread deadlocks for some reason) we have to execute a whole lot of tasks on threads so that the GUI thread typically remains free to process GUI messages, and then tie the completion of those tasks to "event listeners" that update program states and execute the next task in the job. Anyway, exception handling makes this much easier to manage, because I have one single error handler at the GUI message pump, and nearly every possible "recoverable error" (I call them runtime errors, which mimic's STL's naming convention) propagate into that single catch block (errors from GUI or threads alike). From there I have the proper thread affinity needed to pop up modal dialogs (which feed the user an error and request a response), re-check all the various program states (and a full recheck is always needed, because with so many threaded, message driven tasks I can't really make any assumptions safely), and then re-dispatch new messages to respond to the user's request related to the error.

Without exception handling, I'd have ~1500 more lines of code in the new wxgui for PCSX2, most of which would be designed to check for and propagate a myriad of errors up the thread callstacks and across threads in a fashion that would be able to transmit relevant error information to the main GUI thread for display to the user. And I'd have to include manual cleanup for hundreds of objects, all of which are currently handled safely and implicitly by C++ object destructors. So yeah, the flow control argument is only true for kozy coders who've only had to work on nice linear uninterpretable algorithms that only return a possibility of 1 or 2 errors. (in which case yes, using exceptions is pretty pointless -- man wish I could be so lucky).

Quote:Turning on exceptions adds data to each binary produced, increasing compile time (probably slightly) and possibly increasing address space pressure.
Interesting, but applies to GCC only. Structured Exceptions supported by MSVC and Intel C/C++ have no such penalty. Unfortunately they're under patent by Borland currently, but that'll expire eventually, in which case everyone will have access to minimal-overhead SEH exceptions that can safely throw across almost any type of stackframe. I do wonder if Google's anti-exception rant is more to do with their hate of patents and Microsoft in general, and less to do with actual programming justifications.

Quote:The availability of exceptions may encourage developers to throw them when they are not appropriate or recover from them when it's not safe to do so. For example, invalid user input should not cause exceptions to be thrown. We would need to make the style guide even longer to document these restrictions!
Whine, whine. Show me a programing language that can be used to write compact and modularized code and I'll show you a programming language that an idiot coder can abuse in a hundred ways to make entirely obfuscated code. Hell, the worst code I've ever had to try and read only used the most basic forms of C/C++ syntax and did not break a single basic tenant of the typical modern style guides. But it was still horribly written, had twisted redundant logic patterns, and no underlying structure in spite of what appeared to be, on the surface, a structured API. That code didn't even use the ternary ?: operator, for which many "style guide" people like to whine about to no end as well.

Point is, this is what programming is all about. you're given tools and you need to be educated on how to use them. If you're given a hammer and a screwdriver, and if you pound a bunch of nails into a wood board at random then you're Fail as a coder. Fine. If someone then gives you a power saw and you proceed to lop off your arms and legs, that doesn't mean we need to make power saws illegal. It means we should probably make sure you don't re-contribute your gene pool to the next generation of human beings. But that won't happen either. Exceptions are a power saw. They're really good at cutting through the meat of error handling. And yea if you're not using the table guard, you're going to probably bleed to death on the ambulance on your way to Coder Hell. But that won't get much sympathy from me. Wink

I will point out two real drawbacks of Exceptions, that Google missed in their stereotypical anti-exception zealot rant:

* C#'s exception structure SUCKS. By that I mean the way it defines and uses it's exception object types. It provides for no easy way to isolate Logic Errors (typically considered programmer errors, or debug assertions) from Runtime Errors (typically safely recoverable, such as file/stream errors). In fact, I don't think Logic Errors should be exceptions at all. They should be debug-build assertions, and should result in program failure in non debug builds (typically there is no "safe" recovery from any logic error, regardless of how robust your app is. Once a program encounters a logic error it's a safe bet the app's internal data structures have been compromised). That's a case of Microsoft making a gross error in the design of their error handling, and has nothing to do with the merits of exception handling itself. The same thing could be done in any API by providing poorly designed error codes, and not properly checking for and handling logic errors.

* Exceptions can complicate 3rd party API "upgradability." Although this actually applies to most object-oriented APIs, and not excpetions specifically. That is, it's more a side-effect of the "benefits" of strict typing, object encapsulation, and the complications in retaining compat up the tree of such object hierarchies. All of these things effect exceptions (which are objects) much like any other object. So if you realize your exception model is busted (like C#'s is), you're screwed for finding a nice way to fix it that won't force rewrites of existing code wanting to upgrade to the new lib.

[edit: and this was a big reason why, until recently, many 3rdparty libraries didn't embrace OOP]

... of course moderm programming has fixed THAT too, since it's becoming ever-more commonplace for "side-by-side" versions of shared libraries. This is made possible by the mass storage available these days, and basically library authors say "if you want the old APi, use the old code, kthx." and that too, is a good thing, where the overall progress of software design and technology is concerned. But I'm sure a lot of coders find reasons to ***** about it too. Wink
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply
#15
I'll also add that the Object Unwind/Destructor Cleanup model is the only safe way to code any threaded application that allows for the cancellation of threaded tasks. And typically planning for thread cancellation is a Good Thing for "robust software design."

Sometimes you can get away with using Processes instead of threads, in which case you can rely on other more absolute resource cleanup systems provided by the Operating System. this is what Google Chrome does, for example. That's all well and good, when you have a situation where the parallel tasks needn't communicate much data between each other; and a web browser is a great fit for exactly that. In PCSX2's case, the threads communicate craploads of data, so running them as individual processes would be a huge speed hit.

(and ironically, such design means Chrome can do virtually no proper code cleanup, C++ exceptions or not, and still get away scott free because the operating system ensures complete process heap cleanup when the window is closed -- in which case their first excuse is, again, rendered moot).

Anyway my point is a lot of modern programming application and multi-core programming design requires thinking in terms of object cleanup anyway; whether you explicitly use exceptions or not.
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply
#16
Air: Ah, Thanks for filling me in, seems fairly reasonable. I just often forget how truly performance intensive emulators really are.
Reply
#17
There is one thing, specific to C++ exceptions, that is also a rather unfortunate drawback:

* C++ does not execute the object destructor when an exception occurs within the object constructor.

This can lead to a lot of unwitted memory leakage. Take, for example:

Code:
class Fail
{
protected:
    MyObject*  m_obj1, m_obj2;

public:
    Fail()
    {
        m_obj1 = m_obj2 = NULL;

        m_obj1 = new MyObject();
        m_obj2 = new MyObject();
        throw Exception::FileNotFound( "SomeDumbFile.txt" );
    }

    ~Fail()
    {
        delete m_obj1;
        delete m_obj2;
    }
};

int main()
{
    try {
        Fail righthere;
    }
    catch( Exception::FileNotFound& )
    {
    }
}

... it looks like it's cleaning up, except the destructor never calls. So it will leak two copies of MyObject with what seems like almost no way to clean it up. One approach you might end up doing is to put asinine red tape into every constructor to execute the destructor on any exception:

Code:
Fail()
    {
        try {
        m_obj1 = m_obj2 = NULL;

        m_obj1 = new MyObject();
        m_obj2 = new MyObject();
        throw Exception::FileNotFound( "SomeDumbFile.txt" );
        } catch(...)
        {
            _cleanup();
            throw;            // re-throw exception for handling somewhere up the stack.
        }
    }

    ~Fail() { _cleanup(); }

    void _cleanup() throw()
    {
        delete m_obj1;
        delete m_obj2;
    }

That works, but the recommended approach by the C++ people is to use an autoptr (except it sucks, and you're better off rolling your own or using boost::smart_ptr). But yea, that ends up working out a lot nicer:

Code:
class Fail
{
protected:
    SmartPtr<MyObject>  m_obj1, m_obj2;

public:
    Fail()
    {
        m_obj1 = new MyObject();
        m_obj2 = new MyObject();
        throw Exception::FileNotFound( "SomeDumbFile.txt" );
    }

    ~Fail() throw()
    {
    }
};

So, C++ executes destructors for embedded objects, but not for the parent object who's constructor is actually throwing the exception. This is one of the primary reasons why C++ Gurus always preach "Never have a Naked Pointer." And really, having pointers wrapped in SmartPtr<> is basically what modern languages like Java, C#, and Python do internally automatically. As you might notice, it does work out to be less red tape in the long run. No need to NULL-ify pointers, and no need to delete either. So the resulting code is quite a bit more compact.
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply
#18
Also, wxWidgets uses internal "heap" managers of sorts for its window objects. All objects derive from wxObject, and each one's constructor adds itself to an encapsulated std::list, who's destructor is reliably called during exceptions. So the whole list of children objects and windows belonging to any window will get cleaned up. Smile wxWidgets objects also have reference counting features, although those are fairly useless if the references aren't properly unlinked due to missing destructor calls.

To be honest, I've had more problems with memory leaks in reference-counted systems than via C++ Object Destructor management. At least with traditional object management I can use existing debugging tools to see exactly where leaked blocks spawn and fail to be free'd. With reference counting it can be difficult to impossible to figure out where the leaked references are coming from. For example, there's a reference leak in the DX10 driver for GSdx that keeps the DX10 Device from ever free'ing, and no one can find it as of yet, because the references are nested inside multiple DX objects, many of which we have no source code for. By contrast, there were multiple leaks in DX9 but it only took me an hour or so to lock them all down and fix them using the basic MSVCRT debugging macros.

I've also had issues with Garbage Collection for the same reason (difficult to trace where the leaked references are coming from). There are now entire tools and tutorials on how to debug the .NET GC for leaked references, because it's such a common problem. And GC has also a very nasty drawback for today's multi-core CPUs:

* A Garbage Collector must stop all threads that have access to the garbage collected heap, so that it can perform collections and memory compacting.

As you might guess, that is a serious performance hit in an ever increasingly-parallel CPU universe. Sad Stopping four cores of processing to execute a single-thread task means losing a lot of CPU cycles. There are ways around that by using multiple GC-Heaps that are specific to various worker threads, but none of the primary languages or devtools as of yet actually supports that. (oops)

I used to really buy into the theory of the garbage collector, but as it turns out they are (like a lot of things) only really useful for specific isolated tasks -- and using them as a blanket solution is generally fail.
Jake Stine (Air) - Programmer - PCSX2 Dev Team
Reply




Users browsing this thread: 1 Guest(s)