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


109

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

내가 아는 바에 따르면 예외는 여러 번 반환하는 것과 동일하지만 각 반환 후에 다른 작업을 수행해야하는지 중지해야하는지 여부도 확인합니다. 반납 중지시기를 어떻게 확인합니까? 예외 유형과 스택 위치를 보유하는 두 번째 스택이 있다고 생각합니다. 그런 다음 도착할 때까지 반환합니다. 나는 또한이 두 번째 스택이 건 드리는 유일한 시간이 던지기와 각 시도 / 잡기 때라고 추측하고 있습니다. 리턴 코드로 유사한 동작을 구현하는 AFAICT는 동일한 시간이 걸립니다. 그러나 이것은 모두 추측 일 뿐이므로 실제로 어떤 일이 발생하는지 알고 싶습니다.

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



답변:


105

추측하는 대신에 저는 작은 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_기능이 수행하는 작업이 필요한 경우 원래 사양을 참조하십시오.


23
요약. 예외가 발생하지 않으면 비용이 발생하지 않습니다. 예외가 발생할 때 약간의 비용이 들지만 '이 비용이 오류 처리 코드로 돌아가는 오류 코드를 사용하고 테스트하는 것보다 더 큰가'입니다.
Martin York

5
오류 비용은 실제로 더 클 수 있습니다. 예외 코드는 여전히 디스크에있을 수 있습니다! 오류 처리 코드가 일반 코드에서 제거되었으므로 오류가 아닌 경우 캐시 동작이 향상됩니다.
MSalters

ARM과 같은 일부 프로세서에서 "bl"[분기 및 링크, "호출"이라고도 함] 명령어를 지나서 8 개의 "추가"바이트 주소로 반환하면 다음 주소로 바로 돌아 오는 것과 같은 비용이 듭니다. "bl". 나는 단순히 각 "bl"뒤에 "들어오는 예외"처리기의 주소를 갖는 효율성이 테이블 기반 접근 방식의 효율성과 비교할 수 있는지, 그리고 어떤 컴파일러가 그런 일을 수행하는지 궁금합니다. 내가 볼 수있는 가장 큰 위험은 일치하지 않는 호출 규칙으로 인해 이상한 동작이 발생할 수 있다는 것입니다.
supercat

2
@supercat : 그런 식으로 예외 처리 코드로 I- 캐시를 오염시키고 있습니다. 결국 예외 처리 코드와 테이블이 일반 코드에서 멀리 떨어져있는 이유가 있습니다.
CesarB

1
@CesarB : 각 호출 다음에 하나의 명령어. 특히 "외부"코드만을 사용하는 예외 처리 기술은 일반적으로 코드가 항상 유효한 프레임 포인터를 유지하도록 요구한다는 점을 감안할 때 너무 터무니없는 것 같지 않습니다 (어떤 경우에는 0 개의 추가 명령이 필요할 수 있지만 다른 경우에는 더 많은 하나).
supercat

13

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

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

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

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

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

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

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


1
사람들은 느리다는 것이 아니라 예외가 어떻게 구현되고 코드에 대해 무엇을하는지 모르기 때문에 예외를 사용하는 것을 주저 할 것입니다. 그들이 마법처럼 보이는 사실은 금속에 가까운 유형을 많이 괴롭힌다.
speedplane

@speedplane : 내 생각 엔. 그러나 컴파일러의 요점은 하드웨어를 이해할 필요가 없다는 것입니다 (추상 계층을 제공합니다). 현대 컴파일러를 사용하면 최신 C ++ 컴파일러의 모든 측면을 이해하는 한 사람을 찾을 수 있을지 의문입니다. 왜 이해 복잡한 기능 X. 다른 이해 예외이다
마틴 뉴욕

당신은 항상 하드웨어가하는 일에 대해 어느 정도 알고 있어야합니다. 그것은 정도의 문제입니다. C ++ (Java 또는 스크립트 언어를 통해)를 사용하는 많은 사람들은 종종 성능을 위해 사용합니다. 그들에게는 추상화 계층이 상대적으로 투명해야 금속에서 무슨 일이 벌어지고 있는지 알 수 있습니다.
speedplane

@speedplane : 그런 다음 추상화 계층이 설계 상 훨씬 더 얇은 C를 사용해야합니다.
Martin York

12

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

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

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

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


"예외의 오버 헤드는 컴파일러가 각 스택 프레임 (또는 더 정확하게는 범위)에서 어떤 개체를 소멸시켜야 하는지를 추적하기 위해 코드를 생성해야하기 때문에 발생합니다."컴파일러가 반환에서 개체를 소멸시키기 위해 어쨌든 그렇게해야하지 않습니까?

아니요. 반환 주소와 테이블이있는 스택이 주어지면 컴파일러는 스택에있는 함수를 확인할 수 있습니다. 그로부터 어떤 객체가 스택에 있었을까요? 예외가 발생한 후에 수행 할 수 있습니다. 약간 비싸지 만 실제로 예외가 발생할 때만 필요합니다.
MSalters

웃기게도 "각 스택 프레임이 객체 수, 유형, 이름을 추적하여 내 함수가 스택을 파고 디버깅하는 동안 상속 된 범위를 확인할 수 있다면 멋지지 않을까요?" , 그리고 어떤면에서 이것은 그런 일을하지만, 수동으로 항상 테이블을 모든 범위의 첫 번째 변수로 선언하지 않습니다.
Dmitry


5

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



0

모든 좋은 대답.

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

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


공통적으로 동의하지만 경우에 따라 예외가 코드를 단순화 할 수 있습니다. 생성자의 오류 처리에 대해 생각해보세요 ...-다른 방법은 a) 참조 매개 변수로 오류 코드를 반환하거나 b) 전역 변수를 설정하는 것입니다
Uhli
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.