为了账号安全,请及时绑定邮箱和手机立即绑定

C ++中的异常如何工作(在幕后)

C ++中的异常如何工作(在幕后)

繁星coding 2019-12-09 09:47:03
我一直看到人们说例外情况很慢,但我从未见过任何证据。因此,我将询问异常在后台如何工作,而不是询问它们是否存在,因此我可以决定何时使用它们以及它们是否缓慢。据我所知,异常与执行一堆返回是相同的事情,但是它还会检查何时需要停止执行返回。如何检查何时停止?我正在猜测,并说有第二个堆栈保存异常类型,然后堆栈位置返回直到到达那里。我还猜测唯一一次堆栈接触是在掷球和每次尝试/接球。AFAICT使用返回代码实现类似行为将花费相同的时间。但这只是一个猜测,所以我想知道。异常如何真正起作用?
查看完整描述

3 回答

?
慕的地6264312

TA贡献1817条经验 获得超6个赞

不用猜测,我决定实际上是用一小段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,得出的结论是:无论是否引发异常,开销都是额外的空间(用于处理程序和表),加上在引发异常时解析表并执行处理程序的额外开销。如果使用异常而不是错误代码,并且错误很少见,则错误速度会更快,因为您不再需要进行错误测试。


查看完整回答
反对 回复 2019-12-09
?
茅侃侃

TA贡献1842条经验 获得超21个赞

例外是缓在旧时代真的。
在大多数现代编译器中,这不再成立。

注意:仅仅因为我们有例外并不意味着我们也不会使用错误代码。如果可以在本地处理错误,请使用错误代码。当错误需要更多上下文来更正时,请使用异常:我在这里雄辩地写道:指导异常处理策略的原则是什么?

当不使用任何异常时,异常处理代码的成本实际上为零。

引发异常时,将完成一些工作。
但是您必须将其与返回错误代码并一路检查它们以指出可以处理错误的位置的开销进行比较。两者都花费更多的时间来编写和维护。

对于新手来说也有一个陷阱:
尽管Exception对象应该很小,但是有些人却在其中放了很多东西。然后,您需要复制异常对象。解决方案有两个方面:

  • 不要把多余的东西放在例外中。

  • 通过const引用捕获。

在我看来,我敢打赌,带有例外的同一代码将比没有例外的代码更有效率,或者至少具有可比性(但具有检查功能错误结果的所有额外代码)。请记住,您没有免费获得任何东西,编译器正在生成您应首先编写的用于检查错误代码的代码(通常,编译器比人类更有效)。


查看完整回答
反对 回复 2019-12-09
?
天涯尽头无女友

TA贡献1831条经验 获得超9个赞

有多种方法可以实现异常,但是通常它们将依赖于操作系统的某些基础支持。在Windows上,这是结构化异常处理机制。

有关代码项目的详细信息,进行了不错的讨论:C ++编译器如何实现异常处理

发生异常的开销是因为,如果异常传播到该范围之外,则编译器必须生成代码来跟踪必须在每个堆栈帧(或更确切地说是范围)中销毁哪些对象。如果函数在堆栈上没有需要调用析构函数的局部变量,则它不应因异常处理而降低性能。

使用返回码一次只能解开堆栈的单个级别,而如果在中间堆栈帧中无事可做,则异常处理机制可以在一次操作中进一步跳回堆栈。


查看完整回答
反对 回复 2019-12-09
  • 3 回答
  • 0 关注
  • 697 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信