我一直看到有人说异常很慢,但我从来没有看到任何证据.因此,我不会询问它们是否存在,而是会询问异常是如何在场景背后起作用的,因此我可以决定何时使用它们以及它们是否很慢.
据我所知,异常与做一堆返回是一回事,但它也会检查何时需要停止返回.它如何检查何时停止?我正在猜测并说有一个第二个堆栈,其中包含异常类型和堆栈位置然后返回直到它到达那里.我也猜测堆栈触摸的唯一时间是抛出和每次尝试/捕获.使用返回代码实现类似行为的AFAICT将花费相同的时间.但这都是猜测,所以我想知道.
例外如何真正起作用?
我决定用一小段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:
惊喜!普通代码路径上根本没有额外的指令.编译器生成额外的外部修复代码块,通过函数末尾的表引用(实际上放在可执行文件的单独部分).所有的工作都是由标准库在幕后完成的,基于这些表(_ZTI11MyException
是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_
功能的信息,请参阅它们的原始规范:
安腾C++ ABI
在过去,例外情况缓慢是正确的.
在大多数现代编译器中,这不再成立.
注意:仅仅因为我们有异常并不意味着我们也不使用错误代码.当可以在本地处理错误时使用错误代码.当错误需要更多上下文进行更正时使用异常:我在这里更加雄辩地写了:指导您的异常处理策略的原则是什么?
当没有使用异常时,异常处理代码的代价几乎为零.
抛出异常时,会完成一些工作.
但是你必须将它与返回错误代码的成本进行比较,并将它们一直检查到可以处理错误的位置.编写和维护都更耗时.
新手还有一个问题:
虽然Exception对象应该很小,但有些人会在其中放入大量内容.然后,您需要复制异常对象的成本.解决方案有两个方面:
不要在你的例外中加入额外的东西.
通过const引用捕获.
在我看来,我敢打赌,具有异常的相同代码要么更高效,要么至少与没有异常的代码相当(但是有所有额外的代码来检查函数错误结果).记住你没有得到任何免费的东西,编译器生成你应该首先编写的代码来检查错误代码(通常编译器比人类更有效).
您可以通过多种方式实现异常,但通常它们将依赖于操作系统的一些底层支持.在Windows上,这是结构化的异常处理机制.
关于代码项目的细节有很好的讨论:C++编译器如何实现异常处理
出现异常的开销是因为如果异常传播出该范围,编译器必须生成代码以跟踪必须在每个堆栈帧(或更精确的范围)中破坏哪些对象.如果函数在堆栈上没有需要调用析构函数的局部变量,那么它不应该在异常处理时具有性能损失.
使用返回代码一次只能展开堆栈的单个级别,而异常处理机制可以在一次操作中进一步向下跳转,如果在中间堆栈帧中没有任何内容可以执行.
Matt Pietrek撰写了一篇关于Win32结构化异常处理的优秀文章.虽然本文最初是在1997年编写的,但它今天仍然适用(但当然只适用于Windows).
本文探讨的问题,基本上认为,在实践中有一个运行时的成本例外,虽然成本是相当低的,如果没有抛出异常.好文章,推荐.