programing tip

C ++에서 예외가 작동하는 방식 (뒤에서)

itbloger 2020. 8. 8. 11:41
반응형

C ++에서 예외가 작동하는 방식 (뒤에서)


사람들이 예외가 느리다고 말하는 것을 계속 보지만 증거는 없습니다. 따라서 예외가 있는지 묻는 대신 예외가 어떻게 작동하는지 묻습니다. 그러면 예외를 언제 사용할지, 느리게 사용할지 결정할 수 있습니다.

내가 아는 바에 따르면 예외는 많은 반환을 수행하는 것과 동일하지만 반환을 중지해야 할 때도 확인합니다. 언제 중지해야하는지 어떻게 확인합니까? 나는 추측을하고 예외 유형을 보유하는 두 번째 스택이 있다고 말하고 스택 위치는 거기에 도착할 때까지 반환합니다. 나는 또한 스택이 터치되는 유일한 시간이 던지고 모든 시도 / 잡기에 있다고 추측하고 있습니다. 리턴 코드로 유사한 동작을 구현하는 AFAICT는 동일한 시간이 소요됩니다. 그러나 이것은 모두 추측이므로 알고 싶습니다.

예외는 실제로 어떻게 작동합니까?


추측하는 대신에 저는 작은 C ++ 코드와 약간 오래된 Linux 설치로 생성 된 코드를 실제로 살펴보기로 결정했습니다.

class MyException
{
public:
    MyException() { }
    ~MyException() { }
};

void my_throwing_function(bool throwit)
{
    if (throwit)
        throw MyException();
}

void another_function();
void log(unsigned count);

void my_catching_function()
{
    log(0);
    try
    {
        log(1);
        another_function();
        log(2);
    }
    catch (const MyException& e)
    {
        log(3);
    }
    log(4);
}

로 컴파일 g++ -m32 -W -Wall -O3 -save-temps -c하고 생성 된 어셈블리 파일을 살펴 보았습니다.

    .file   "foo.cpp"
    .section    .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
    .align 2
    .p2align 4,,15
    .weak   _ZN11MyExceptionD1Ev
    .type   _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
    pushl   %ebp
.LCFI0:
    movl    %esp, %ebp
.LCFI1:
    popl    %ebp
    ret
.LFE7:
    .size   _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev

_ZN11MyExceptionD1Ev이다 MyException::~MyException()컴파일러는 소멸자의 인라인이 아닌 사본을 필요로 결정, 그래서.

.globl __gxx_personality_v0
.globl _Unwind_Resume
    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_catching_functionv
    .type   _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ebx
.LCFI4:
    subl    $20, %esp
.LCFI5:
    movl    $0, (%esp)
.LEHB0:
    call    _Z3logj
.LEHE0:
    movl    $1, (%esp)
.LEHB1:
    call    _Z3logj
    call    _Z16another_functionv
    movl    $2, (%esp)
    call    _Z3logj
.LEHE1:
.L5:
    movl    $4, (%esp)
.LEHB2:
    call    _Z3logj
    addl    $20, %esp
    popl    %ebx
    popl    %ebp
    ret
.L12:
    subl    $1, %edx
    movl    %eax, %ebx
    je  .L16
.L14:
    movl    %ebx, (%esp)
    call    _Unwind_Resume
.LEHE2:
.L16:
.L6:
    movl    %eax, (%esp)
    call    __cxa_begin_catch
    movl    $3, (%esp)
.LEHB3:
    call    _Z3logj
.LEHE3:
    call    __cxa_end_catch
    .p2align 4,,3
    jmp .L5
.L11:
.L8:
    movl    %eax, %ebx
    .p2align 4,,6
    call    __cxa_end_catch
    .p2align 4,,6
    jmp .L14
.LFE9:
    .size   _Z20my_catching_functionv, .-_Z20my_catching_functionv
    .section    .gcc_except_table,"a",@progbits
    .align 4
.LLSDA9:
    .byte   0xff
    .byte   0x0
    .uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
    .byte   0x1
    .uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
    .uleb128 .LEHB0-.LFB9
    .uleb128 .LEHE0-.LEHB0
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB1-.LFB9
    .uleb128 .LEHE1-.LEHB1
    .uleb128 .L12-.LFB9
    .uleb128 0x1
    .uleb128 .LEHB2-.LFB9
    .uleb128 .LEHE2-.LEHB2
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB3-.LFB9
    .uleb128 .LEHE3-.LEHB3
    .uleb128 .L11-.LFB9
    .uleb128 0x0
.LLSDACSE9:
    .byte   0x1
    .byte   0x0
    .align 4
    .long   _ZTI11MyException
.LLSDATT9:

놀라다! 일반 코드 경로에는 추가 지침이 전혀 없습니다. 대신 컴파일러는 함수 끝에있는 테이블 (실제로는 실행 파일의 별도 섹션에 있음)을 통해 참조되는 추가 라인 외부 수정 코드 블록을 생성했습니다. 모든 작업은 이러한 테이블 ( _ZTI11MyExceptionis typeinfo for MyException)을 기반으로 표준 라이브러리에 의해 백그라운드에서 수행됩니다 .

좋아, 그것은 실제로 나에게 놀라운 일은 아니었다. 나는 이미이 컴파일러가 어떻게했는지 알고 있었다. 어셈블리 출력을 계속합니다.

    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_throwing_functionb
    .type   _Z20my_throwing_functionb, @function
_Z20my_throwing_functionb:
.LFB8:
    pushl   %ebp
.LCFI6:
    movl    %esp, %ebp
.LCFI7:
    subl    $24, %esp
.LCFI8:
    cmpb    $0, 8(%ebp)
    jne .L21
    leave
    ret
.L21:
    movl    $1, (%esp)
    call    __cxa_allocate_exception
    movl    $_ZN11MyExceptionD1Ev, 8(%esp)
    movl    $_ZTI11MyException, 4(%esp)
    movl    %eax, (%esp)
    call    __cxa_throw
.LFE8:
    .size   _Z20my_throwing_functionb, .-_Z20my_throwing_functionb

Here we see the code for throwing an exception. While there was no extra overhead simply because an exception might be thrown, there is obviously a lot of overhead in actually throwing and catching an exception. Most of it is hidden within __cxa_throw, which must:

  • Walk the stack with the help of the exception tables until it finds a handler for that exception.
  • Unwind the stack until it gets to that handler.
  • Actually call the handler.

Compare that with the cost of simply returning a value, and you see why exceptions should be used only for exceptional returns.

To finish, the rest of the assembly file:

    .weak   _ZTI11MyException
    .section    .rodata._ZTI11MyException,"aG",@progbits,_ZTI11MyException,comdat
    .align 4
    .type   _ZTI11MyException, @object
    .size   _ZTI11MyException, 8
_ZTI11MyException:
    .long   _ZTVN10__cxxabiv117__class_type_infoE+8
    .long   _ZTS11MyException
    .weak   _ZTS11MyException
    .section    .rodata._ZTS11MyException,"aG",@progbits,_ZTS11MyException,comdat
    .type   _ZTS11MyException, @object
    .size   _ZTS11MyException, 14
_ZTS11MyException:
    .string "11MyException"

The typeinfo data.

    .section    .eh_frame,"a",@progbits
.Lframe1:
    .long   .LECIE1-.LSCIE1
.LSCIE1:
    .long   0x0
    .byte   0x1
    .string "zPL"
    .uleb128 0x1
    .sleb128 -4
    .byte   0x8
    .uleb128 0x6
    .byte   0x0
    .long   __gxx_personality_v0
    .byte   0x0
    .byte   0xc
    .uleb128 0x4
    .uleb128 0x4
    .byte   0x88
    .uleb128 0x1
    .align 4
.LECIE1:
.LSFDE3:
    .long   .LEFDE3-.LASFDE3
.LASFDE3:
    .long   .LASFDE3-.Lframe1
    .long   .LFB9
    .long   .LFE9-.LFB9
    .uleb128 0x4
    .long   .LLSDA9
    .byte   0x4
    .long   .LCFI2-.LFB9
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI3-.LCFI2
    .byte   0xd
    .uleb128 0x5
    .byte   0x4
    .long   .LCFI5-.LCFI3
    .byte   0x83
    .uleb128 0x3
    .align 4
.LEFDE3:
.LSFDE5:
    .long   .LEFDE5-.LASFDE5
.LASFDE5:
    .long   .LASFDE5-.Lframe1
    .long   .LFB8
    .long   .LFE8-.LFB8
    .uleb128 0x4
    .long   0x0
    .byte   0x4
    .long   .LCFI6-.LFB8
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI7-.LCFI6
    .byte   0xd
    .uleb128 0x5
    .align 4
.LEFDE5:
    .ident  "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
    .section    .note.GNU-stack,"",@progbits

Even more exception handling tables, and assorted extra information.

So, the conclusion, at least for GCC on Linux: the cost is extra space (for the handlers and tables) whether or not exceptions are thrown, plus the extra cost of parsing the tables and executing the handlers when an exception is thrown. If you use exceptions instead of error codes, and an error is rare, it can be faster, since you do not have the overhead of testing for errors anymore.

In case you want more information, in particular what all the __cxa_ functions do, see the original specification they came from:


Exceptions being slow was true in the old days.
In most modern compiler this no longer holds true.

Note: Just because we have exceptions does not mean we do not use error codes as well. When error can be handled locally use error codes. When errors require more context for correction use exceptions: I wrote it much more eloquently here: What are the principles guiding your exception handling policy?

The cost of exception handling code when no exceptions are being used is practically zero.

When an exception is thrown there is some work done.
But you have to compare this against the cost of returning error codes and checking them all the way back to to point where the error can be handled. Both more time consuming to write and maintain.

Also there is one gotcha for novices:
Though Exception objects are supposed to be small some people put lots of stuff inside them. Then you have the cost of copying the exception object. The solution there is two fold:

  • Don't put extra stuff in your exception.
  • Catch by const reference.

In my opinion I would bet that the same code with exceptions is either more efficient or at least as comparable as the code without the exceptions (but has all the extra code to check function error results). Remember you are not getting anything for free the compiler is generating the code you should have written in the first place to check error codes (and usually the compiler is much more efficient than a human).


There are a number of ways you could implement exceptions, but typically they will rely on some underlying support from the OS. On Windows this is the structured exception handling mechanism.

There is decent discussion of the details on Code Project: How a C++ compiler implements exception handling

The overhead of exceptions occurs because the compiler has to generate code to keep track of which objects must be destructed in each stack frame (or more precisely scope) if an exception propagates out of that scope. If a function has no local variables on the stack that require destructors to be called then it should not have a performance penalty wrt exception handling.

Using a return code can only unwind a single level of the stack at a time, whereas an exception handling mechanism can jump much further back down the stack in one operation if there is nothing for it to do in the intermediate stack frames.


Matt Pietrek wrote an excellent article on Win32 Structured Exception Handling. While this article was originally written in 1997, it still applies today (but of course only applies to Windows).


This article examines the issue and basically finds that in practice there is a run-time cost to exceptions, although the cost is fairly low if the exception isn't thrown. Good article, recommended.


A friend of me wrote a bit how Visual C++ handles exceptions some years ago.

http://www.xyzw.de/c160.html


All good answers.

Also, think about how much easier it is to debug code that does 'if checks' as gates at the top of methods instead of allowing the code to throw exceptions.

My motto is that it's easy to write code that works. The most important thing is to write the code for the next person who looks at it. In some cases, it's you in 9 months, and you don't want to be cursing your name!

참고URL : https://stackoverflow.com/questions/307610/how-do-exceptions-work-behind-the-scenes-in-c

반응형