当前位置:  开发笔记 > 编程语言 > 正文

在c ++中,异常如何工作(幕后)

如何解决《在c++中,异常如何工作(幕后)》经验,为你挑选了5个好方法。

我一直看到有人说异常很慢,但我从来没有看到任何证据.因此,我不会询问它们是否存在,而是会询问异常是如何在场景背后起作用的,因此我可以决定何时使用它们以及它们是否很慢.

据我所知,异常与做一堆返回是一回事,但它也会检查何时需要停止返回.它如何检查何时停止?我正在猜测并说有一个第二个堆栈,其中包含异常类型和堆栈位置然后返回直到它到达那里.我也猜测堆栈触摸的唯一时间是抛出和每次尝试/捕获.使用返回代码实现类似行为的AFAICT将花费相同的时间.但这都是猜测,所以我想知道.

例外如何真正起作用?



1> CesarB..:

我决定用一小段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:

惊喜!普通代码路径上根本没有额外的指令.编译器生成额外的外部修复代码块,通过函数末尾的表引用(实际上放在可执行文件的单独部分).所有的工作都是由标准库在幕后完成的,基于这些表(_ZTI11MyExceptiontypeinfo 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_功能的信息,请参阅它们的原始规范:

安腾C++ ABI


总结一下.如果没有抛出异常,则不需要花费.抛出异常会产生一些成本,但问题是"这个成本是否大于使用和测试错误代码一直回到错误处理代码".
错误成本确实可能更高.异常代码很可能仍在磁盘上!由于从正常代码中删除了错误处理代码,因此非错误情况下的缓存行为得到改善.

2> Martin York..:

在过去,例外情况缓慢正确的.
在大多数现代编译器中,这不再成立.

注意:仅仅因为我们有异常并不意味着我们也不使用错误代码.当可以在本地处理错误时使用错误代码.当错误需要更多上下文进行更正时使用异常:我在这里更加雄辩地写了:指导您的异常处理策略的原则是什么?

当没有使用异常时,异常处理代码的代价几乎为零.

抛出异常时,会完成一些工作.
但是你必须将它与返回错误代码的成本进行比较,并将它们一直检查到可以处理错误的位置.编写和维护都更耗时.

新手还有一个问题:
虽然Exception对象应该很小,但有些人会在其中放入大量内容.然后,您需要复制异常对象的成本.解决方案有两个方面:

不要在你的例外中加入额外的东西.

通过const引用捕获.

在我看来,我敢打赌,具有异常的相同代码要么更高效,要么至少与没有异常的代码相当(但是有所有额外的代码来检查函数错误结果).记住你没有得到任何免费的东西,编译器生成你应该首先编写的代码来检查错误代码(通常编译器比人类更有效).



3> Rob Walker..:

您可以通过多种方式实现异常,但通常它们将依赖于操作系统的一些底层支持.在Windows上,这是结构化的异常处理机制.

关于代码项目的细节有很好的讨论:C++编译器如何实现异常处理

出现异常的开销是因为如果异常传播出该范围,编译器必须生成代码以跟踪必须在每个堆栈帧(或更精确的范围)中破坏哪些对象.如果函数在堆栈上没有需要调用析构函数的局部变量,那么它不应该在异常处理时具有性能损失.

使用返回代码一次只能展开堆栈的单个级别,而异常处理机制可以在一次操作中进一步向下跳转,如果在中间堆栈帧中没有任何内容可以执行.



4> Greg Hewgill..:

Matt Pietrek撰写了一篇关于Win32结构化异常处理的优秀文章.虽然本文最初是在1997年编写的,但它今天仍然适用(但当然只适用于Windows).



5> Alastair..:

本文探讨的问题,基本上认为,在实践中有一个运行时的成本例外,虽然成本是相当低的,如果没有抛出异常.好文章,推荐.

推荐阅读
手机用户2402851155
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有