c++ 中的异常如何(在幕后)工作?
我不断看到人们说异常很慢,但我从未看到任何证据。 因此,我不会问它们是否是,而是会问异常在幕后如何工作,这样我就可以决定何时使用它们以及它们是否很慢。
据我所知,异常与多次返回相同,只是它还在每次返回后检查是否需要执行另一次返回或停止。 它如何检查何时停止返回? 我猜想还有第二个堆栈保存异常的类型和堆栈位置,然后它会返回直到到达那里。 我还猜测第二个堆栈唯一被触及的时间是在抛出和每次尝试/捕获时。 AFAICT 使用返回代码实现类似的行为将花费相同的时间。 但这只是猜测,所以我想知道到底发生了什么。
异常是如何真正发挥作用的?
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 scenes, so I can make decisions of when to use them and whether they are slow.
From what I know, exceptions are the same as doing a return bunch of times, except that it also checks after each return whether it needs to do another one or to stop. How does it check when to stop returning? I guess there is a second stack that holds the type of the exception and a stack location, it then does returns until it gets there. I am also guessing that the only time this second stack is touched is on a throw and on each try/catch. AFAICT implementing a similar behaviour with return codes would take the same amount of time. But this is all just a guess, so I want to know what really happens.
How do exceptions really work?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
我没有猜测,而是决定使用一小段 C++ 代码和有点旧的 Linux 安装来实际查看生成的代码。
我用 g++ -m32 -W -Wall -O3 -save-temps -c 编译它,并查看生成的汇编文件。
_ZN11MyExceptionD1Ev
是MyException::~MyException()
,因此编译器决定需要析构函数的非内联副本。惊喜! 正常的代码路径上根本没有额外的指令。 相反,编译器生成了额外的外线修复代码块,通过函数末尾的表引用(实际上放在可执行文件的单独部分)。 所有工作均由标准库根据这些表在幕后完成(
_ZTI11MyException
是MyException 的 typeinfo
)。好吧,这对我来说并不意外,我已经知道这个编译器是如何做到的。 继续看汇编输出:
这里我们看到抛出异常的代码。 虽然仅仅因为可能抛出异常而不会产生额外的开销,但在实际抛出和捕获异常时显然存在大量开销。 其中大部分隐藏在
__cxa_throw
中,它必须:将其与简单返回值的成本进行比较,您就会明白为什么异常应该仅用于异常返回。
最后,汇编文件的其余部分:
typeinfo 数据。
更多异常处理表和各种额外信息。
因此,结论是,至少对于 Linux 上的 GCC 来说:无论是否抛出异常,成本都是额外的空间(对于处理程序和表),加上抛出异常时解析表和执行处理程序的额外成本。 如果您使用异常而不是错误代码,并且错误很少见,那么速度可能会更快,因为您不再需要测试错误的开销。
如果您想了解更多信息,特别是所有
__cxa_
函数的功能,请参阅它们来自的原始规范: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.
I compiled it with
g++ -m32 -W -Wall -O3 -save-temps -c
, and looked at the generated assembly file._ZN11MyExceptionD1Ev
isMyException::~MyException()
, so the compiler decided it needed a non-inline copy of the destructor.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
istypeinfo for MyException
).OK, that was not actually a surprise for me, I already knew how this compiler did it. Continuing with the assembly output:
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: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.
In case you want more information, in particular what all the
__cxa_
functions do, see the original specification they came from:在过去,异常缓慢是确实如此。
在大多数现代编译器中,这不再适用。
注意:仅仅因为我们有异常并不意味着我们也不使用错误代码。 当错误可以在本地处理时,使用错误代码。 当错误需要更多上下文来纠正时,请使用异常:我在这里写得更加雄辩:指导异常处理策略的原则是什么?
当不使用异常时,异常处理代码的成本几乎为零。
当抛出异常时,就会完成一些工作。
但是您必须将其与返回错误代码并一直检查它们直到可以处理错误的位置的成本进行比较。 编写和维护都比较耗时。
对于新手来说还有一个陷阱:
尽管 Exception 对象应该很小,但有些人在里面放了很多东西。 然后你就有了复制异常对象的成本。 解决方案有两个:
在我看来,我敢打赌,带有异常的相同代码要么更有效,要么至少与没有异常的代码具有可比性(但具有所有额外的代码来检查函数错误结果)。 请记住,您不会免费获得任何东西,编译器正在生成您应该首先编写的代码来检查错误代码(通常编译器比人类更有效率)。
Exceptions being slow was true 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:
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).
有多种方法可以实现异常,但通常它们将依赖于操作系统的一些底层支持。 在 Windows 上,这是结构化异常处理机制。
关于代码项目的详细信息有不错的讨论:How a C++编译器实现异常处理
发生异常的开销是因为编译器必须生成代码来跟踪如果异常传播到每个堆栈帧(或更精确的范围)中必须销毁哪些对象(如果异常传播到该范围之外)。 如果函数在堆栈上没有需要调用析构函数的局部变量,那么它不应该在异常处理方面有性能损失。
使用返回代码一次只能展开堆栈的一层,而如果在中间堆栈帧中无事可做,则异常处理机制可以在一次操作中跳回堆栈。
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.
There is decent discussion of the details on Code Project: How a C++ compiler implements exception handling
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.
Matt Pietrek 在 Win32 结构化异常处理。 虽然本文最初写于 1997 年,但今天仍然适用(但当然仅适用于 Windows)。
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).
这篇文章研究了这个问题,基本上发现在实践中存在运行时成本异常,尽管如果不抛出异常,成本相当低。 好文章,推荐。
This article examines 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.
我的一个朋友几年前写过一些 Visual C++ 如何处理异常的文章。
http://www.xyzw.de/c160.html
A friend of me wrote a bit how Visual C++ handles exceptions some years ago.
http://www.xyzw.de/c160.html
所有好的答案。
另外,请考虑一下,调试将“if 检查”作为方法顶部的门的代码比允许代码抛出异常要容易得多。
我的座右铭是编写有效的代码很容易。 最重要的是为下一个查看代码的人编写代码。 在某些情况下,9个月后就是你,你不想咒骂你的名字!
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.
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!