首页 文章

c中的异常如何工作(幕后)

提问于
浏览
99

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

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

例外如何真正起作用?

7 回答

  • 13

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

    对代码项目的细节进行了不错的讨论:How a C++ compiler implements exception handling

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

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

  • 1

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

    _ZN11MyExceptionD1EvMyException::~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_ 函数的功能,请参阅它们的原始规范:

  • 2

    This article检查了这个问题,并且基本上发现在实践中存在异常的运行时成本,尽管如果不抛出异常,成本相当低 . 好文章,推荐 .

  • 101

    Matt Pietrek写了一篇关于Win32 Structured Exception Handling的精彩文章 . 虽然本文最初是在1997年编写的,但它今天仍然适用(但当然仅适用于Windows) .

  • 5

    我的一位朋友写了几年前Visual C如何处理异常 .

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

  • 12

    过去的例外情况很慢 was 在过去是如此 .
    在大多数现代编译器中,这不再成立 .

    注意:仅仅因为我们有异常并不意味着我们也不使用错误代码 . 当可以在本地处理错误时使用错误代码 . 当错误需要更多上下文进行更正时使用异常:我在这里更加雄辩地写了:What are the principles guiding your exception handling policy?

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

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

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

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

    • 通过const引用捕获 .

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

  • 6

    所有好的答案 .

    另外,考虑调试代码的容易程度,这些代码在方法顶部执行“if checks”作为门,而不是允许代码抛出异常 .

    我的座右铭是编写有效的代码很容易 . 最重要的是为下一个查看它的人编写代码 . 在某些情况下,这是你在9个月内,你不想诅咒你的名字!

相关问题