在c ++中,exception是如何工作的(在幕后)

我一直看到人们说exception缓慢,但我从来没有看到任何证据。 因此,我不会问是否会出现exception情况,所以我可以决定何时使用它们,以及是否缓慢。

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

exception是如何工作的?

而不是猜测,我决定用一小段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() _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: 

惊喜! 在正常的代码path上没有任何额外的指令。 编译器会生成额外的外联fixup代码块,通过函数末尾的一个表(这实际上放在可执行文件的一个单独部分)引用。 所有的工作都由标准库在幕后完成,基于这些表( _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 

在这里,我们看到抛出exception的代码。 虽然没有额外的开销,只是因为可能会抛出exception,但实际上抛出和捕获exception显然有很多开销。 大部分隐藏在__cxa_throw ,它必须:

  • 在exception表的帮助下走栈,直到find该exception的处理程序。
  • 展开堆栈直到到达处理程序。
  • 其实调用处理程序。

将其与简单地返回一个值的成本进行比较,你就会明白为什么exception只能用于特殊的回报。

完成后,汇编文件的其余部分:

  .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 

甚至更多的exception处理表和各种额外的信息。

因此,至less对于Linux上的GCC,结论是:无论是否引发exception,代价都是额外的空间(处理程序和表),再加上在抛出exception时分析表和执行处理程序的额外成本。 如果您使用exception而不是错误代码,并且错误很less,那么速度更快 ,因为您不再有错误testing的开销。

如果你想要更多的信息,尤其是__cxa_函数的function,请参阅它们来自的原始规范:

  • Itanium C ++ ABI

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

注意:仅仅因为我们有例外,并不意味着我们也不使用错误代码。 当错误可以在本地处理时使用错误代码。 当错误需要更多的上下文进行纠正时,请使用例外:我在这里写得更有说服力: 指导exception处理策略的原则是什么?

当没有例外被使用时,exception处理代码的代价几乎为零。

当抛出exception时,就完成了一些工作。
但是,您必须将其与返回错误代码的成本进行比较,并将其一直检查到可以处理错误的位置。 编写和维护都耗费更多的时间。

新手也有一个问题:
尽pipeException对象应该是很小的,但有些人却把很多东西放在里面。 然后你有复制exception对象的代价。 解决scheme有两个方面:

  • 不要在你的例外中join额外的东西。
  • 通过const引用来捕获。

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

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

代码项目细节的讨论很多: C ++编译器如何实现exception处理

发生exception的开销是因为编译器必须生成代码来跟踪哪些对象必须在每个堆栈框架(或更确切地说是作用域)中被破坏,如果exception传播出该范围。 如果一个函数在栈上没有需要调用析构函数的局部variables,那么在exception处理时不应该有性能损失。

使用返回代码一次只能展开一个级别的堆栈,而在一个操作中,如果在中间堆栈框架中没有任何事情需要处理,exception处理机制可以进一步向下跳回堆栈。

Matt Pietrek写了一篇关于Win32结构化exception处理的优秀文章。 虽然这篇文章最初是在1997年写的,但它现在仍然适用(但当然只适用于Windows)。

这篇文章检查了这个问题,基本上发现在实践中,对exception有一个运行时成本,但是如果不抛出exception,成本相当低。 好文章,推荐。

我的一个朋友写了一些,几年前Visual C ++如何处理exception。

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

所有好的答案。

另外,请考虑debugging在方法顶部执行“如果检查”作为门的代码而不是允许代码抛出exception的代码。

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