Sun Solutions by Forsythe
Jarod Jenson
Chief Technology Architect

Exception-al Programming

Thu, 10/09/2008 - 18:42 by Jarod Jenson

Every now and then, I get in one of those moods where I vent against some abomination of programming that just kills performance. One of my more routine rants was against cond_broadcast()/pthread_cond_broadcast()/notifyAll() - depending on your language and platform of choice.

Well, now I have a new nemesis – exception based versus conditional programming in Java. What do I mean by this? It seems to be a fairly popular practice to abuse Java's robust exception handling by using exceptions to determine program flow and to handle errors that aren't exactly exceptional situations.

Take the following for example. You are tasked with writing a processing engine that can decode various file formats. Maybe you start writing methods to process JPEG's, PDF's, and plain text files. In each method, if you can't deal with the file (for whatever reason) – you throw an application specific exception. Seems fairly logical. Then comes time to put everything together in a generic framework that can handle any of these file types. Here goes the code (simplified for readability by non-programmers):

    int tries = 3;

    try {
        processJPEG(file);
    } catch (Exception e) {
        debug(“Not a JPEG”);
        tries--;
    }

    try {
        processPDF(file);
    } catch (Exception e) {
        debug(“Not a PDF”);
        tries--;
    }

    try {
        processPlain(file);
    } catch (Exception e) {
        debug(“Not a Plain file”);
        tries--;
    }

    if (tries == 0) {
        throw new MyUnknownFileException;
    }

Okay, this is a really silly example; but the sad part is – it is a paraphrase of real code that I ran into. In any valid case, you are taking two exceptions just to actually deal with the file (not to mention there could have been “real” exceptions that were accidentally (and inexplicably) caught.

Is there a better way – well certainly. Let's use real conditionals to do the job in a sane manner. Just change the processing engines to return an error condition (not an exception) if they can't handle the files. The only exceptions they should be throwing are real exceptions (like IOException). Then our code becomes (again simplified):

    try {
        if (!processJPEG(file)) {
            if (!processPDF(file))) {
                if (!processPlain(file)) {
                    throw new MyUnknownFileException;
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

In this case, we only get an exception when there is really a problem. Clearly, this is a very rudimentary example of the issue, but you get the general idea. I don't really know why this is getting popular, but someone (unrelated to this) told me that it was to satisfy some weird Java programming style (not acknowledged by most I know) that methods only have a single exit. Exceptions are not really considered exits (by those subscribing to this model) and are a “cleaner” way to “leave” a method. Whatever.

So why are these exceptions bad? Well, taking an exception is not a trivial process. When an exception occurs, there is a tremendous amount of work done behind the scenes. Memory is allocated, a stack backtrace is generated (in native code) and placed in the exception object, there are potential synchronization contention issues, etc. All of this can add up to  significant wasted CPU cycles. In one application I looked at, there were thousands of exceptions being generated per second. All of them together accounted for almost half of the total CPU consumption. Clearly not the intent of the author and a significant inhibitor of performance of the application.

Also, in the first (broken) example above, the exceptions are caught so there is no clear outward manifestation of the problem, and it tends to go unnoticed. Well, what if we wanted to notice them? The easiest way is, of course, with DTrace.

For the Sun JVM, let's simply insert a probe in the native code that will get executed when we take an exception. The following script will give us per second counts of the exceptions we take by thread id:

<SNIP exceptions.d>

#!/usr/sbin/dtrace -s

pid$1::JVM_FillInStackTrace:entry
{
    @ex[tid] = count();
}

tick-1s
{
    printa(@ex);
    clear(@ex);
}

</SNIP>

So what is a high exception rate? Well it is hard to come up with a specific number, but in my opinion; any exceptions that are not really exceptional cases are wrong. Especially when you are trying to write code that has a high performance requirement.

As a side note, we made other improvements to the code above. First of all, they were opening and closing the file in each processing engine. It should just be kept open to avoid the overhead of the open(2)/close(2) calls. Secondly, they were processing 95% plain text files. We moved that to be the first test. And lastly, which was not implemented while I was there - and hopefully did get implemented - is that many file types have what is called “magic”. That is, the first few bytes will let you know what the file type is without having to do much work. This would be far better as the amount of data read would drop significantly.



Comments

Thank you I've often raised

Thank you
I've often raised the issue to development where I see lots (hundreds or thousands) of exceptions thrown in a single transaction. One issue I still have is that I haven't been able to quantify the impact, in terms of CPU or response time. This makes it more difficult to convince developers that it should be addressed, since everything is functionally correct. Any tips in that area?

Certainly. Just modify the

Certainly. Just modify the script above to get timing information. Something as simple as:

#!/usr/sbin/dtrace -s

pid$1::JVM_FillInStackTrace:entry
{
self->ts = timestamp;
}

pid$1::JVM_FillInStackTrace:return
/ self->ts /
{
@[tid] = quantize(timestamp - self->ts);
self->ts = 0;
}

This will give you a per tid breakdown. Bear in mind, this doesn't account for all the work done. But it should suffice to show it is non-trivial.