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

여기에 예외를 던지는 코드가 있습니다. 단순히 예외가 발생할 수 있기 때문에 추가 오버 헤드가 없었지만 실제로 예외를 던지고 잡는 데에는 분명히 많은 오버 헤드가 있습니다. 대부분은 다음 __cxa_throw을 수행해야합니다.

  • 해당 예외에 대한 핸들러를 찾을 때까지 예외 테이블의 도움으로 스택을 살펴보십시오.
  • 해당 핸들러에 도달 할 때까지 스택을 푸십시오.
  • 실제로 핸들러를 호출하십시오.

값을 단순히 반환하는 비용과 비교하면 예외가 예외적 인 반환에만 사용되어야하는 이유를 알 수 있습니다.

완료하려면 나머지 어셈블리 파일 :

    .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"

typeinfo 데이터입니다.

    .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

더 많은 예외 처리 테이블 및 여러 추가 정보.

따라서 적어도 Linux의 GCC에 대한 결론은 예외가 발생했는지 여부에 관계없이 추가 공간 (처리기와 테이블의 경우)과 예외가 발생했을 때 테이블을 구문 분석하고 처리기를 실행하는 추가 비용입니다. 오류 코드 대신 예외를 사용하고 오류가 드물다면 더 이상 오류를 테스트하는 오버 헤드가 없기 때문에 더 빠를 수 있습니다 .

더 많은 정보, 특히 모든 __cxa_기능이 수행하는 작업이 필요한 경우 원래 사양을 참조하십시오.


답변

예전에는 느린 예외 사실 이었습니다 .
대부분의 현대 컴파일러에서 이것은 더 이상 사실이 아닙니다.

참고 : 예외가 있다고해서 오류 코드도 사용하지 않는다는 의미는 아닙니다. 오류를 로컬에서 처리 할 수있는 경우 오류 코드를 사용합니다. 오류 수정을 위해 더 많은 컨텍스트가 필요한 경우 예외 사용 : 예외 처리 정책을 이끄는 원칙은 무엇입니까?

예외가 사용되지 않는 경우 예외 처리 코드의 비용은 거의 0입니다.

예외가 발생하면 일부 작업이 수행됩니다.
그러나이를 오류 코드를 반환하고 오류를 처리 할 수있는 지점까지 다시 확인하는 비용과 비교해야합니다. 작성 및 유지 관리에 더 많은 시간이 소요됩니다.

또한 초보자를위한 한 가지 문제가 있습니다.
예외 객체는 작지만 일부 사람들은 그 안에 많은 것을 넣습니다. 그런 다음 예외 개체를 복사하는 비용이 있습니다. 해결책은 두 가지입니다.

  • 예외에 추가 항목을 넣지 마십시오.
  • const 참조로 잡아라.

제 생각에는 예외가있는 동일한 코드가 예외가없는 코드보다 더 효율적이거나 적어도 예외가없는 코드만큼 비슷하다고 생각합니다 (하지만 함수 오류 결과를 확인하는 모든 추가 코드가 있음). 컴파일러가 오류 코드를 확인하기 위해 처음에 작성해야하는 코드를 생성하는 것은 무료로 제공되지 않는다는 것을 기억하십시오 (일반적으로 컴파일러는 사람보다 훨씬 효율적입니다).


답변

예외를 구현할 수있는 방법에는 여러 가지가 있지만 일반적으로 OS의 일부 기본 지원에 의존합니다. Windows에서 이것은 구조화 된 예외 처리 메커니즘입니다.

Code Project : C ++ 컴파일러가 예외 처리를 구현하는 방법 에 대한 세부 사항에 대한 적절한 논의가 있습니다.

예외의 오버 헤드는 예외가 해당 범위를 벗어나 전파되는 경우 컴파일러가 각 스택 프레임 (또는 더 정확하게는 범위)에서 어떤 개체를 소멸시켜야하는지 추적하기 위해 코드를 생성해야하기 때문에 발생합니다. 함수에 소멸자를 호출해야하는 지역 변수가 스택에없는 경우 예외 처리와 관련된 성능 저하가 없어야합니다.

반환 코드를 사용하면 한 번에 한 수준의 스택 만 해제 할 수있는 반면, 예외 처리 메커니즘은 중간 스택 프레임에서 수행 할 작업이없는 경우 한 번의 작업으로 스택 아래로 훨씬 더 뒤로 이동할 수 있습니다.


답변

Matt Pietrek는 Win32 Structured Exception Handling 에 대한 훌륭한 기사를 작성했습니다 . 이 기사는 원래 1997 년에 작성되었지만 오늘날에도 여전히 적용됩니다 (물론 Windows에만 적용됨).


답변

이 기사 는 문제를 조사하고 기본적으로 예외가 발생하지 않으면 비용이 상당히 낮지 만 실제로 예외에 대한 런타임 비용이 있음을 발견합니다. 좋은 기사, 추천합니다.


답변

내 친구가 몇 년 전에 Visual C ++에서 예외를 처리하는 방법을 약간 썼습니다.

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


답변

모든 좋은 대답.

또한, 코드가 예외를 던지도록 허용하는 대신 메소드 상단에서 게이트로 ‘if checks’를 수행하는 코드를 디버그하는 것이 얼마나 쉬운 지 생각해보십시오.

제 좌우명은 작동하는 코드를 작성하기 쉽다는 것입니다. 가장 중요한 것은 그것을 보는 다음 사람을 위해 코드를 작성하는 것입니다. 어떤 경우에는 9 개월 안에 당신이고, 당신은 당신의 이름을 저주하고 싶지 않습니다!