底层如何实现异常?

73

几乎每个人都使用它们,但是许多人,包括我在内,只是想当然地认为它们可以正常工作。

我正在寻找高质量的资料。我使用的语言有: Java、C、C#、Python、C++,所以这些对我来说最感兴趣。

现在,由于你可以在 C++ 中投入任何东西,所以它可能是一个很好的起点。

此外,C 语言接近汇编语言。如何使用纯 C 结构和没有汇编语言来模拟异常?

最后,我听说谷歌员工在某些项目中不使用异常,因为速度考虑。这只是传言吗?如果没有异常,如何完成任何实质性的工作?

谢谢。


5
我知道至少有一名谷歌员工说异常在.NET中并不慢:Jon Skeet。 - R. Martinho Fernandes
5
Google遵循他们的风格指南,在C++中避免使用异常:http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions - Adam Goode
4
使用返回代码可以避免使用异常,但您必须仔细检查结果。请注意,不要改变原意。 - Martin York
7
在当前gcc的实现中,当异常未被抛出时(即在正常处理下没有成本(或者非常少)),异常与普通代码的速度相同。 - Martin York
3
为什么他们要避开他们? - cschol
显示剩余8条评论
10个回答

51

异常只是更一般的高级非局部流程控制结构的一个具体例子。其他例子包括:

  • 通知(异常的一般化,最初源自一些旧的Lisp对象系统,现在在例如CommonLisp和Ioke中实现),
  • continuations(一种更结构化的GOTO形式,在高级、高阶语言中广泛使用),
  • coroutines(子程序的一般化形式,特别流行于Lua中),
  • Python式的generators(基本上是协程的一种受限形式),
  • fibers(协作轻量级线程),当然还有前面提到的GOTO

(我相信还有许多我错过的例子。)

这些结构的一个有趣的特性是它们在表达能力方面大致等价:如果你拥有其中的任何一个,你就可以很容易地构建出其他所有结构。

因此,如何最好地实现异常取决于您可用的其他结构:

  • 每个CPU都有GOTO,因此如果必须的话,您总是可以退回到那里。
  • C语言有setjmp/longjmp,它们基本上是MacGyver的continuations(用胶带和牙签构建的,不是真正的东西,但至少可以在您没有更好选择的情况下让您摆脱眼前的麻烦)。
  • JVM和CLI都有自己的异常处理机制,这意味着如果您的语言的异常语义与Java/C#的匹配,您就可以轻松搞定(但如果不匹配,则您就很惨了)。
  • Parrot VM既有异常,又有continuations。
  • Windows有其自己的异常处理框架,语言实现者可以使用它来构建自己的异常处理机制。
一个非常有趣的用例,引发了对异常使用和实现的探讨,这个用例是微软 Live Lab 的 Volta 项目(现已停止)。Volta 的目标是通过按下一个按钮为 Web 应用程序提供架构重构。因此,您可以通过在 .NET 代码上放置一些 `[Browser]` 或 `[DB]` 属性来将单层 Web 应用程序转变为双层或三层应用程序,然后该代码将自动运行在客户端或数据库中。为了做到这一点,.NET 代码必须被翻译成 JavaScript 源代码。
现在,你可以只是在 JavaScript 中编写整个虚拟机并运行未经修改的字节码。 (基本上,将 CLR 从 C++ 移植到 JavaScript)。实际上有一些这样的项目(例如 HotRuby VM),但这既效率低下,而且与其他 JavaScript 代码不太互操作。
所以,他们编写了一个编译器,将 CIL 字节码编译为 JavaScript 源代码。然而,JavaScript 缺少某些 .NET 具有的特性(生成器、线程,还有两个异常模型不完全兼容),更重要的是它缺少编译器作者喜欢的某些特性(要么是 GOTO,要么是 Continuation),这些功能可以用于实现上述缺失的功能。
然而,JavaScript 有异常。因此,他们使用 JavaScript 异常来实现 Volta Continuations,然后使用 Volta Continuations 来实现 .NET 异常、.NET 生成器甚至是 .NET 托管线程(!!!)
所以,回答您最初的问题:
异常的实现是讽刺的!至少在这个非常特定的情况下是这样。

另一个很好的例子是Go邮件列表上关于异常提案,它使用Goroutines来实现异常(类似于并发协程和CSP进程混合的东西)。还有一个例子是Haskell,它使用Monads、惰性求值、尾递归优化和高阶函数来实现异常。一些现代CPU也支持异常的基本构建模块(例如专门为Azul Systems Java Compute Accelerators设计的Vega-3 CPU)。


1
太棒了,这是一门应用信息学的短期课程!非常感谢,Herzlichen Dank! - Mikhail Poda
1
是的,我从中学到了很多东西。此外,Exceptions在实现时让我大开眼界(更不用说托管线程了)。谢谢。 - Inversus

24

这里是C++异常常用的实现方式:
http://www.codesourcery.com/public/cxx-abi/abi-eh.html

虽然该文档是为Itanium架构编写的,但此处描述的实现方法同样适用于其他架构。需要注意的是,由于C++异常比较复杂,因此该文档相对较长。

这里有关于LLVM如何实现异常的良好描述:
http://llvm.org/docs/ExceptionHandling.html

由于LLVM旨在成为许多运行时的通用中间表示,因此所描述的机制可以应用于许多语言。


2
这两个链接都是很好的参考资料。 - Richard Pennington
2
第一个链接又坏了。 - Carsten S
新链接:https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html - Blair Houghton

19
在他的书中C Interfaces and Implementations: Techniques for Creating Reusable Software中,D.R. Hanson使用一组宏和setjmp/longjmp提供了一个不错的在纯C中实现异常处理的方法。他提供了TRY/RAISE/EXCEPT/FINALLY宏,可以模拟几乎所有C++异常处理的功能并且更多。
代码可以在此处查看(查看except.h/except.c)
附带说明,关于Google的问题,他们的员工实际上被允许在新代码中使用异常处理,而在旧的代码中禁用的官方原因是因为已经以那种方式编写了,并且混用不合适。
个人认为,没有异常处理的C++并不是最好的选择。

有没有关于谷歌员工在他们的代码中使用异常的任何来源? - Limbo Peng

9

C/C++编译器使用底层的操作系统工具进行异常处理。像.NET或Java这样的框架也在虚拟机中依赖于操作系统工具。例如,在Windows中,真正的重活是由SEH(Structured Exception Handling)基础设施完成的。您绝对应该阅读旧的参考文章:《Win32™结构化异常处理深入浅出》

至于不使用异常的成本,它们很昂贵,但与什么相比呢?与返回错误代码相比?在考虑正确性和代码质量的成本后,商业应用程序始终会选择异常。除了一些非常关键的操作系统级别的功能外,异常总体上都更好。

最后,还有一个反模式,即将异常用于流程控制。异常应该是异常情况,滥用异常来控制流程的代码将付出性能代价。


2
结构化异常和C++异常不是同一种东西。 - Martin York
2
C++异常是作为SEH实现的。要理解C++异常,就需要了解SEH。 - Remus Rusanu
2
顺便说一句,当一个关于异常的问题在它的标题中包含“under the hood”时,如果不链接到Matt Pietrek的文章,那就是一种错误...http://blogs.msdn.com/matt_pietrek/ - Remus Rusanu

6

有关异常(底层实现)的最佳论文是Barbara Liskov和Alan Snyder的CLU中的异常处理。每次我开始编写新的编译器时,都会参考它。

对于在C中使用setjmplongjmp实现的较高级别的视图,我推荐Dave Hanson的C接口与实现(像Eli Bendersky一样)。


1
你每次需要这份文件都要支付19美元吗? - Hamish Grubijan
6
不,我有一份纸质复印件放在盒子里,或者我可以通过我的大学图书馆获取它。专业学会为再版收取如此离谱的费用,而所有的写作、编辑和审稿工作都是由志愿者完成的,这是一种可悲的欺诈行为。 - Norman Ramsey

5

setjmp()longjmp()通常用于IT技术中。

捕获异常确实会带来一定的成本,但对于大多数情况而言,这并不是一个大问题。


啊哈!http://www.cs.utk.edu/~huangj/cs360/360/notes/Setjmp/lecture.html 欢迎提供更多细节。 - Hamish Grubijan

4
异常实现需要处理的关键问题是如何在抛出异常后返回到异常处理程序。由于在C++中自try语句以来可能已经进行了任意数量的嵌套函数调用,因此必须展开调用堆栈以搜索处理程序。无论如何实现,都必须承担维护足够信息以执行此操作的代码大小成本(通常对于可以引发异常的调用而言,这意味着数据表)。这也意味着动态代码执行路径将比仅从函数调用返回更长(在大多数平台上,这是一项相当廉价的操作)。根据实现方式,可能还会产生其他成本。
与使用的语言有关的相对成本将有所不同。使用的高级语言越高,代码大小成本就越不重要,并且可以保留信息,而不管是否使用异常。
一个应用程序,在其中出于良好原因经常避免使用异常(和C++)的是嵌入式固件。在典型的小型裸机或RTOS平台中,您可能有1MB的代码空间,或者64K,甚至更小。有些平台非常小,甚至使用C也不切实际。在这种环境中,大小影响是相关的,因为上述成本。它还影响标准库本身。嵌入式工具链供应商通常会生成一个没有异常能力的库,这对代码大小有巨大影响。高度优化的编译器还可以分析调用图并优化掉所需的调用帧信息,以实现相当大的空间减少。异常还使得分析硬实时要求更加困难。
在更典型的环境中,代码大小成本几乎肯定是无关紧要的,而性能因素很可能是关键。是否使用它们将取决于您的性能要求以及如何使用它们。在非异常情况下使用异常可以产生优雅的设计,但是对于高性能系统来说,性能成本可能是不可接受的。实现和相关成本将根据平台和编译器而异,因此真正了解异常是否成为问题的最佳方法是分析自己代码的性能。

3
谷歌的C++代码(除了一些特定的Windows情况)不使用异常:请参考指南,简单来说就是“我们不使用C++异常”。以下是讨论中的引用(点击箭头展开URL):
我们反对使用异常的建议并非基于哲学或道德立场,而是实际原因。因为我们希望在谷歌使用我们的开源项目,如果这些项目使用异常,那么将很难做到,因此我们需要在谷歌开源项目中反对使用异常。如果我们从头开始做可能会有所不同。
这个规则不适用于谷歌在其他语言中的代码,如Java和Python。

如果我理解正确的话,那就意味着在过去异常是被禁止的。现在,虽然在新的代码中技术上允许使用异常,但由于这些新代码需要与不使用异常的旧代码进行交互,所以它们仍然无法使用。 - Jörg W Mittag
@Jörg,如果你在Google编写C++代码(除了一些特定于Windows的异常情况),如果使用异常,你将无法提交它--这种限制根源于过去,但现在仍然存在。发布的指南侧重于开源代码,但它们通常与我们在Google编写的其他代码相同。 - Alex Martelli
为什么在C++中,异常会使得代码难以开源,而在其他语言中却不会呢? - Casebash
@casebash,本质上,正如我所解释的那样,这是关于实现质量问题的(特别是在过去,正如我所提到的):Java和Python的实现从来没有出现过异常问题(在这些语言中处理它们也更容易,参见Sutter的“Exceptional C++”以了解在C++中正确处理它们所需的细微差别)。 - Alex Martelli

1
关于性能 - 稀疏使用异常可能会产生可忽略的影响,但不要滥用它们。
我个人见过 Java 代码,由于在重要循环中使用了异常而比其本应有的性能差了两个数量级(花费了大约 x100 的时间),而更标准的 if/returns 则可以更好地解决这个问题。

1
另一方面,如果重要的、大型的、付费的企业客户在紧密的、热循环中实际使用它们,JVM 实现者才会费心地使异常更快。哦,好吧。有趣的是,HotSpot 已经对异常做了一些疯狂的事情。特别是,如果 throwcatch 在单个内联代码块中(实际上可以跨越多个方法甚至多个类),那么它只是一个 jmp,也就是说,它的速度与 if/return 完全相同。 - Jörg W Mittag

1
一些运行时(例如Objective-C运行时)具有零成本的64位异常。这意味着进入try块不需要任何代价。但是,当抛出异常时,这会非常昂贵。这遵循“针对平均情况进行优化”的范例 - 异常应该是异常的情况,因此最好使没有异常的情况变得非常快,即使这样做会以明显较慢的异常为代价。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接