异常如何在 C++ 中工作(在幕后)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/307610/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How do exceptions work (behind the scenes) in c++
提问by
I keep seeing people say that exceptions are slow, but I never see any proof. So, instead of asking if they are, I will ask how do exceptions work behind the scene, so I can make a decisions of when to use them and if they are slow.
我一直看到人们说异常很慢,但我从来没有看到任何证据。因此,与其问它们是否是,我会问异常如何在幕后工作,这样我就可以决定何时使用它们以及它们是否很慢。
From what I know, exceptions are the same thing as doing a bunch of return but it also checks when it needs to stop doing the return. How does it check when to do stop? I am taking a guess and saying there is a second stack which holds the type of exception and stack location then does returns until it gets there. I am also guessing the only time that stack is touch is on a throw and every try/catch. AFAICT implementing a similar behaviour with return code would take the same amount of time. But this is all a guess, so I want to know.
据我所知,异常与执行一堆返回相同,但它还会检查何时需要停止返回。它如何检查何时停止?我猜测并说有第二个堆栈保存异常类型和堆栈位置,然后返回直到它到达那里。我也猜测堆栈是触摸的唯一时间是在一次投掷和每次尝试/捕获时。AFAICT 使用返回码实现类似的行为将花费相同的时间。但这都是猜测,所以我想知道。
How do exceptions really work?
异常如何真正起作用?
采纳答案by CesarB
Instead of guessing, I decided to actually look at the generated code with a small piece of C++ code and a somewhat old Linux install.
我没有猜测,而是决定用一小段 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);
}
I compiled it with g++ -m32 -W -Wall -O3 -save-temps -c
, and looked at the generated assembly file.
我用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
is MyException::~MyException()
, so the compiler decided it needed a non-inline copy of the destructor.
_ZN11MyExceptionD1Ev
is 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 , %esp
.LCFI5:
movl .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 , %esp
.LCFI8:
cmpb .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"
, 8(%ebp)
jne .L21
leave
ret
.L21:
movl , (%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
, (%esp)
.LEHB0:
call _Z3logj
.LEHE0:
movl , (%esp)
.LEHB1:
call _Z3logj
call _Z16another_functionv
movl , (%esp)
call _Z3logj
.LEHE1:
.L5:
movl , (%esp)
.LEHB2:
call _Z3logj
addl , %esp
popl %ebx
popl %ebp
ret
.L12:
subl , %edx
movl %eax, %ebx
je .L16
.L14:
movl %ebx, (%esp)
call _Unwind_Resume
.LEHE2:
.L16:
.L6:
movl %eax, (%esp)
call __cxa_begin_catch
movl , (%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:
Surprise! There are no extra instructions at all on the normal code path. The compiler instead generated extra out-of-line fixup code blocks, referenced via a table at the end of the function (which is actually put on a separate section of the executable). All the work is done behind the scenes by the standard library, based on these tables (_ZTI11MyException
is typeinfo for MyException
).
惊喜!正常的代码路径上根本没有额外的指令。相反,编译器生成了额外的外联修复代码块,通过函数末尾的表(实际上放在可执行文件的单独部分)进行引用。所有的工作都由标准库在幕后完成,基于这些表 ( _ZTI11MyException
is typeinfo for MyException
)。
OK, that was not actually a surprise for me, I already knew how this compiler did it. Continuing with the assembly output:
好吧,这对我来说实际上并不奇怪,我已经知道这个编译器是如何做到的。继续汇编输出:
.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
Here we see the code for throwing an exception. While there was no extra overhead simply because an exception might be thrown, there is obviously a lot of overhead in actually throwing and catching an exception. Most of it is hidden within __cxa_throw
, which must:
这里我们看到了抛出异常的代码。虽然仅仅因为可能会抛出异常而没有额外的开销,但在实际抛出和捕获异常时显然有很多开销。其中大部分隐藏在 中__cxa_throw
,必须:
- Walk the stack with the help of the exception tables until it finds a handler for that exception.
- Unwind the stack until it gets to that handler.
- Actually call the handler.
- 在异常表的帮助下遍历堆栈,直到找到该异常的处理程序。
- 展开堆栈,直到它到达该处理程序。
- 实际上调用处理程序。
Compare that with the cost of simply returning a value, and you see why exceptions should be used only for exceptional returns.
将其与简单返回值的成本进行比较,您就会明白为什么异常应该只用于异常返回。
To finish, the rest of the assembly file:
最后,汇编文件的其余部分:
##代码##The typeinfo data.
类型信息数据。
##代码##Even more exception handling tables, and assorted extra information.
更多异常处理表,以及各种额外信息。
So, the conclusion, at least for GCC on Linux: the cost is extra space (for the handlers and tables) whether or not exceptions are thrown, plus the extra cost of parsing the tables and executing the handlers when an exception is thrown. If you use exceptions instead of error codes, and an error is rare, it can be faster, since you do not have the overhead of testing for errors anymore.
因此,结论是,至少对于 Linux 上的 GCC 而言:无论是否抛出异常,成本都是额外的空间(用于处理程序和表),加上在抛出异常时解析表和执行处理程序的额外成本。如果您使用异常而不是错误代码,并且错误很少见,它会更快,因为您不再有测试错误的开销。
In case you want more information, in particular what all the __cxa_
functions do, see the original specification they came from:
如果您想了解更多信息,特别是所有__cxa_
函数的作用,请参阅它们来自的原始规范:
回答by Martin York
Exceptions being slow wastrue in the old days.
In most modern compiler this no longer holds true.
在过去,缓慢的例外是真实的。
在大多数现代编译器中,这不再适用。
Note: Just because we have exceptions does not mean we do not use error codes as well. When error can be handled locally use error codes. When errors require more context for correction use exceptions: I wrote it much more eloquently here: What are the principles guiding your exception handling policy?
注意:仅仅因为我们有异常并不意味着我们也不使用错误代码。当错误可以在本地处理时使用错误代码。当错误需要更多上下文来纠正时,使用异常:我在这里写得更有说服力:指导您的异常处理策略的原则是什么?
The cost of exception handling code when no exceptions are being used is practically zero.
当没有使用异常时,异常处理代码的成本实际上为零。
When an exception is thrown there is some work done.
But you have to compare this against the cost of returning error codes and checking them all the way back to to point where the error can be handled. Both more time consuming to write and maintain.
当抛出异常时,就会完成一些工作。
但是您必须将其与返回错误代码的成本进行比较,并将它们一直检查到可以处理错误的位置。编写和维护都更耗时。
Also there is one gotcha for novices:
Though Exception objects are supposed to be small some people put lots of stuff inside them. Then you have the cost of copying the exception object. The solution there is two fold:
对于新手来说还有一个问题:
尽管 Exception 对象应该很小,但有些人在其中放入了很多东西。那么你就有了复制异常对象的成本。解决方法有两个:
- Don't put extra stuff in your exception.
- Catch by const reference.
- 不要在你的例外中放额外的东西。
- 通过常量引用捕获。
In my opinion I would bet that the same code with exceptions is either more efficient or at least as comparable as the code without the exceptions (but has all the extra code to check function error results). Remember you are not getting anything for free the compiler is generating the code you should have written in the first place to check error codes (and usually the compiler is much more efficient than a human).
在我看来,我敢打赌,带有异常的相同代码要么更高效,要么至少与没有异常的代码具有可比性(但具有检查函数错误结果的所有额外代码)。请记住,您不会免费获得任何东西,编译器会生成您应该首先编写的代码来检查错误代码(通常编译器比人类效率高得多)。
回答by Rob Walker
There are a number of ways you could implement exceptions, but typically they will rely on some underlying support from the OS. On Windows this is the structured exception handling mechanism.
有多种方法可以实现异常,但通常它们将依赖于操作系统的一些底层支持。在 Windows 上,这是结构化异常处理机制。
There is decent discussion of the details on Code Project: How a C++ compiler implements exception handling
Code Project: How a C++ compiler implements exception processing上有详细的讨论
The overhead of exceptions occurs because the compiler has to generate code to keep track of which objects must be destructed in each stack frame (or more precisely scope) if an exception propagates out of that scope. If a function has no local variables on the stack that require destructors to be called then it should not have a performance penalty wrt exception handling.
发生异常的开销是因为编译器必须生成代码来跟踪如果异常传播到该范围之外,则必须在每个堆栈帧(或更准确地说是范围)中销毁哪些对象。如果一个函数在堆栈上没有需要调用析构函数的局部变量,那么它不应该有异常处理的性能损失。
Using a return code can only unwind a single level of the stack at a time, whereas an exception handling mechanism can jump much further back down the stack in one operation if there is nothing for it to do in the intermediate stack frames.
使用返回码一次只能展开一层堆栈,而如果在中间堆栈帧中无事可做,异常处理机制可以在一次操作中进一步跳回堆栈。
回答by Greg Hewgill
Matt Pietrek wrote an excellent article on Win32 Structured Exception Handling. While this article was originally written in 1997, it still applies today (but of course only applies to Windows).
Matt Pietrek 写了一篇关于Win32 结构化异常处理的优秀文章。虽然这篇文章最初写于 1997 年,但它今天仍然适用(但当然只适用于 Windows)。
回答by Alastair
This articleexamines the issue and basically finds that in practice there is a run-time cost to exceptions, although the cost is fairly low if the exception isn't thrown. Good article, recommended.
这篇文章研究了这个问题,基本上发现在实践中异常是有运行时成本的,尽管如果不抛出异常,成本是相当低的。好文章,推荐。
回答by Nils Pipenbrinck
A friend of me wrote a bit how Visual C++ handles exceptions some years ago.
几年前,我的一个朋友写了一些 Visual C++ 处理异常的方法。
回答by Kieveli
All good answers.
所有的好答案。
Also, think about how much easier it is to debug code that does 'if checks' as gates at the top of methods instead of allowing the code to throw exceptions.
此外,想想调试将“if 检查”作为方法顶部的门而不是允许代码抛出异常的代码要容易得多。
My motto is that it's easy to write code that works. The most important thing is to write the code for the next person who looks at it. In some cases, it's you in 9 months, and you don't want to be cursing your name!
我的座右铭是编写有效的代码很容易。最重要的是为下一个看它的人编写代码。在某些情况下,9 个月后就是你,你不想诅咒你的名字!