User: Password:
|
|
Subscribe / Log in / New account

Imaginary losses

Imaginary losses

Posted Mar 27, 2008 20:14 UTC (Thu) by wahern (subscriber, #37304)
In reply to: Imaginary losses by ncm
Parent article: Striking gold in binutils

Modern C++ exceptions might be conceptually zero-cost, but it is not less work than comparable C code. The difference is in how the stack is prepared to call the function. There is, evidently, a small fixed cost in every C++ function call which offsets the lack of a test+jump after the call. I admit I was unfamiliar w/ the details of modern exception handling, but I'm glad you forced my hand, because if anything we're cutting through some hyperbole.

Also, the error handling pattern in my C code doesn't duplicate as much code as the straw man examples posted here. I'm perfectly capable of using "goto" to jump to a common error handling block within a function, achieving something similar to the range table method of placing the error handling logic outside of the main execution flow. And I do this most of the time, because it just makes sense, and I get, IMO, better readability than exceptions, because there are fewer syntactic blocks to obscure my code. (I admit, that's highly subjective.)

Here's the example you requested. I used GCC--gcc version 4.0.1 (Apple Inc. build 5465)--with -O2 optimization. To compile: [cc|c++] -S -02 -o ex.S ex.c -DCPLUSPLUS=[0|1]

#if CPLUSPLUS

#include <iostream>

void noargs(int i) {
        if (i > 1)
                throw i;

        return /* void */;
}

int main (int argc, char *argv[]) {
        try {
                noargs(argc);
        } catch (int e) {
                _Exit(e);
        }

        return 0;
}

#else

#include <stdio.h>

int noargs(int i) {
        if (i > 1)
                return i;

        return 0;
}

int main(int argc, char *arg[]) {
        int e;

        if (0 != (e = noargs(argc))) {
                _Exit(e);
        }

        return 0;
}

#endif

Simple, straight-forward code. Let us count the number of instructions from main() to our call to noargs(), and from return from the noargs() to leaving main().

C++ output:

.globl _main
_main:
LFB1481:
        pushl   %ebp
LCFI4:
        movl    %esp, %ebp
LCFI5:
        pushl   %esi
LCFI6:
        subl    $20, %esp
LCFI7:
        movl    8(%ebp), %eax
        movl    %eax, (%esp)
LEHB0:
        call    __Z6noargsi
LEHE0:
        addl    $20, %esp
        xorl    %eax, %eax
        popl    %esi
        leave
        ret

On the "fast-path", we have 12 instructions for C++.

Now, plain C:

globl _main
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    8(%ebp), %eax
        movl    %eax, (%esp)
        call    _noargs
        testl   %eax, %eax
        jne     L10
        leave
        xorl    %eax, %eax
        ret

And in C, we have... 11 instructions. Well, well! And I'm being charitable, because in fact there are additional instructions for noargs() which increase the disparity: 8 in C, 12 in C++. That makes the total count 19 to 24, but for simplicity's sake, I'm happy to keep things confined to the caller.

Explain to me how this is a poor example. I'm willing to entertain you, and I by no means believe that this little example is conclusive. But, it seems pretty telling to me. I admit, I'm surprised how close they are. Indeed, if anybody suggested to me that C++ exceptions introduced too much of a runtime cost, I'd set them straight. But if they looked me straight in the eye and told me unequivocally that they were faster, I'd show them the door.


(Log in to post comments)

Imaginary losses

Posted Mar 27, 2008 20:56 UTC (Thu) by pphaneuf (subscriber, #23480) [Link]

From my experience, the more common thing is not really try/catch, but letting the exception bubble up. Basically, you just want to clean up and tell your caller something went wrong.

We'll agree that if there is a clean up to do, it's probably equally there in C and in C++, right? The "big saving" in C++ is in the case where you just clean up and bubble up the exception. If a function doesn't have cleaning up to do, it doesn't even go in that function at all!

As they say, the fastest way to do something is to not do it.

Imaginary losses

Posted Mar 27, 2008 21:24 UTC (Thu) by wahern (subscriber, #37304) [Link]

Hmmm, good point. So, if you don't throw from an intermediate function, you compound the
savings.

Well... I guess I'll just call "uncle" at this point. I personally don't like exceptions,
specifically because in my experience letting errors "bubble up" usually means that much error
context is lost, and the programmer gets into the habit of not rigorously handling errors
(that's why, I guess, I didn't think about that pattern). But, in a discussion like this
that's inapplicable.

Imaginary losses

Posted Mar 27, 2008 22:15 UTC (Thu) by pphaneuf (subscriber, #23480) [Link]

My theory is that you do something about it where you can. If you can't think of something useful to work around the problem, then just let it bubble up, maybe someone who knows better will take care of it, and if not, it'll be the same as an assert.

That's clearly sensible in a lot of cases, because otherwise there would be no such thing as error statuses, they'd just all "handle the errors".

I also quite prefer the default failure mode of a programmer failing to handle an error to be a loud BANG than silently going forward...

Imaginary losses

Posted Mar 27, 2008 21:04 UTC (Thu) by wahern (subscriber, #37304) [Link]

I forgot to test multiple calls in the same try{} block. Indeed, for every additional
back-to-back call C needs an additional two instructions (test+jump). So, for moderately long
functions, w/ a single try{} block and lots of calls to some small set of functions, I can see
C++ being faster. The trick is that you don't want the fixed-costs to exceed the gains, of
course. In the above example, C++ pulls ahead at the 4th call to noargs().

It would be an interesting exercise to count the number of function definitions, and functions
call in my code, and multiple by the respective differences of C and C++. But, it seems
complicated by the treatment of blocks in C++. I can see how in some tests C++ came out 15%
ahead, though.

In any event, there is indeed a fixed-cost to C++ exceptions. There might not be a prologue,
but the epilogue is invariably longer for functions, and, apparently, some blocks.


Copyright © 2017, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds