如何在引擎盖下实现异常?

几乎每个人都在使用它们,但包括我在内的许多人只是理所当然地认为它们只是在工作。

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

现在,C + + 可能是一个很好的起点,因为你可以在这种语言中抛出任何东西。

另外,C 接近于汇编。如何使用纯 C 构造而不用汇编来模拟异常?

最后,我听到一个传言,谷歌员工不使用异常的一些项目,由于速度的考虑。这只是谣言吗?没有他们,怎么可能有实质性的成就?

谢谢你。

10156 次浏览

通常是 setjmp()longjmp()

异常捕获确实有不小的成本,但是对于大多数目的来说,这并不是一个大问题。

在他的书 C 接口与实现: 创建可重用软件的技术中,D.R.Hanson 使用一组宏和 setjmp/longjmp在纯 C 中提供了一个很好的异常实现。他提供了 TRY/RAISE/EXCEPT/FINLY 宏,这些宏可以模拟 C + + 异常所做的几乎所有事情,甚至更多。

可以仔细阅读代码 给你(查看 except.h/except.c)。

另外,你关于谷歌的问题。他们的员工实际上被允许在新代码中使用异常,而官方禁止在旧代码中使用异常的原因是因为它已经被这样编写了,所以混合使用样式是没有意义的。

就个人而言,我也认为没有例外的 C + + 不是最好的主意。

C/C + + 编译器使用底层操作系统工具进行异常处理。像这样的框架。在虚拟机中,Net 或 Java 也依赖于操作系统工具。例如,在 Windows 中,真正繁重的工作是由结构化异常处理基础设施 SEH 完成的。你绝对应该阅读旧的参考文章: Win32TM 结构化异常处理深度速成教程

至于不使用异常的成本,它们是昂贵的,但是与什么相比呢?与返回错误代码相比?在考虑到正确性和代码质量的成本之后,异常总是会在商业应用程序中获胜。除了少数非常关键的操作系统级别功能外,异常总是比较好的。

最后但并非最不重要的一点是,存在使用异常进行流控制的反模式。异常应该是例外情况,滥用流控制异常的代码将为性能付出代价。

对于性能稀缺的异常使用,其影响可能可以忽略不计,但是不要滥用它们。

我个人曾经见过一些 Java 代码,它们的执行数量级比实际情况差了两倍(大约花了 x100的时间) ,因为异常被用在了一个重要的循环中,而不是更标准的 if/return。

下面是实现 C + + 异常的一种常见方法:
Http://www.codesourcery.com/public/cxx-abi/abi-eh.html

它用于 Itanium 体系结构,但是这里描述的实现也用于其他体系结构。注意,这是一个很长的文档,因为 C + + 异常是复杂的。

下面是关于 LLVM 如何实现异常的一个很好的描述:
Http://llvm.org/docs/exceptionhandling.html

由于 LLVM 是许多运行时的通用中间表示,所以描述的机制可以应用于许多语言。

Google 的 C + + 代码(除了一些特定于 Windows 的情况)不使用异常: cfr 指引,简称: “我们不使用 C + + 异常”。从讨论中引用(点击箭头展开 URL) :

我们反对使用异常的建议是 不以哲学或 道德上的理由,但是实际上的理由。 因为我们想用我们的 谷歌的开源项目 很难做到这一点,如果这些 项目使用异常,我们需要 对谷歌的例外情况提出建议 还有开源项目 可能会有所不同 从头再来一遍。

此规则不适用于其他语言(如 Java 和 Python)中的 Google 代码。

有些运行时(如 Objective-C 运行时)具有零成本的64位异常。这意味着输入 try 块不需要任何成本。但是,当引发异常时,这样做的代价非常高昂。这遵循了“针对普通情况进行优化”的范例——异常意味着异常,所以最好在没有异常的情况下尽快进行优化,即使这样做的代价是大大减慢异常的速度。

例外情况只是高级非本地流控制构造更一般情况的一个具体例子,其他例子包括:

  • Notions (异常的一般化,最初来自一些旧的 Lisp 对象系统,现在在 CommonLisp 和 Ioke 中实现) ,
  • Continations (GOTO的一种更结构化的形式,在高级、高阶语言中很流行) ,
  • Coroutines (子例程的一种推广,尤其在 Lua 中流行) ,
  • 生成器 à la Python (本质上是协程的一种受限形式) ,
  • 纤维 (合作轻量级线程) ,当然还有前面提到的
  • GOTO.

(我相信还有很多其他的我错过了。)

这些结构的一个有趣的特性是,它们在表达能力方面大致相当: 如果有 ,就可以很容易地构建所有其他结构。

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

  • 每个 CPU 都有 GOTO,因此,如果必须的话,您总是可以回到 GOTO
  • C 有 setjmp/longjmp,基本上是 MacGyver 的延续(用胶带和牙签做的,不是真的,但如果你没有更好的东西,至少可以让你摆脱眼前的麻烦)。
  • JVM 和 CLI 有它们自己的异常,这意味着如果语言的异常语义与 Java/C # 的语义匹配,那么您就是自由的(但如果不匹配,那么您就完蛋了)。
  • Parrot VM 作为异常和延续。
  • Windows 有自己的异常处理框架,语言实现者可以使用该框架在上面构建自己的异常。

一个非常有趣的用例,异常的 用途还有异常的 实施都是微软 Live 实验室的 Volta 项目。(现已解散)Volta 的目标是在一个按钮的推动下为 Web 应用程序提供架构重构。因此,您可以将一层 Web 应用程序转换为两层或三层应用程序,只需将一些 [Browser][DB]属性放在您的。NET 代码,然后代码将自动运行在客户端或数据库中。为了做到这一点,。NET 代码必须转换成 JavaScript 源代码,这是显而易见的。

现在,可以只需用 JavaScript 编写一个完整的虚拟机,并在未修改的情况下运行字节码。(基本上,将 CLR 从 C + + 移植到 JavaScript。)实际上有一些项目可以做到这一点(比如 HotRuby VM) ,但是这种方法不仅效率低下,而且与其他 JavaScript 代码的互操作性也不强。

因此,他们编写了一个编译器,将 CIL 字节码编译成 JavaScript 源代码。然而,JavaScript 缺乏某些特性。NET 具有(生成器,线程,还有两个异常模型不是100% 兼容) ,更重要的是,它缺乏编译器作者 (无论是 GOTO或延续)的特定功能,可以用来实现上述缺失的功能。

然而,JavaScript是的也有例外。因此,他们使用 JavaScript 异常来实现 伏特延续,然后使用 伏特延续来实现 .NET 异常.NET 生成器甚至 .NET 托管线程(! ! !)

回答你最初的问题:

如何在引擎盖下实现异常?

具有讽刺意味的是,除外! 至少在这个特殊的情况下,无论如何。

另一个很好的例子是 Go 邮件列表中的一些异常建议,它们使用 Goroutines 实现异常(类似于并发协程和 CSP 进程的混合)。另一个例子是 Haskell,它使用 Monads、延迟计算、尾部调用优化和高阶函数来实现异常。一些现代的 CPU 还支持异常的基本构建块(例如,专门为 Azul Systems Java 计算加速器设计的 Vega-3 CPU)。

异常实现需要处理的关键问题是在抛出异常后如何返回到异常处理程序。由于自 C + + 中的 try 语句以来,您可能已经进行了任意数量的嵌套函数调用,因此它必须使用 解除调用堆栈来搜索处理程序。无论如何实现,这必须导致 代码大小成本代码大小成本维护足够的信息以执行此操作(并且通常意味着可以接受异常的调用的数据表)。它还意味着动态代码 执行路径将更长不仅仅是从函数调用返回(在大多数平台上这是一个相当廉价的操作)。根据实施情况,还可能有其他成本。

相对成本将根据所使用的语言而变化。所使用的高级语言,代码大小成本的影响就越小,而且无论是否使用异常,都可以保留信息。

嵌入式固件是一种应用程序,它常常因为一些好的原因而避免使用异常(通常是 C + +)。在典型的小型裸机或 RTOS 平台中,您可能有1MB 的代码空间,或者64K,甚至更小。有些平台太小了,甚至 C 语言都不实用。在这种环境中,由于上面提到的成本,尺寸的影响是相关的。它还会影响标准库本身。嵌入式工具链供应商通常会生成一个没有异常能力的库,这对代码大小有很大的影响。高度优化的编译器还可以分析调用图,并优化掉所需的调用帧信息,以便进行大量的放松操作,从而减少空间。异常还使得分析硬实时需求变得更加困难。

在更典型的环境中,代码大小成本几乎肯定是不相关的,性能因素可能是关键。您是否使用它们将取决于您的性能要求以及您希望如何使用它们。在非异常情况下使用异常可以实现优雅的设计,但是性能成本对于高性能系统来说可能是不可接受的。实现和相对成本因平台和编译器而异,因此真正理解异常是否存在问题的最佳方法是分析自己代码的性能。

有关异常的 实施(在引擎盖下)的最好的论文是由 Barbara Liskov 和 Alan Snyder 写的 CLU 中的异常处理。我每次启动一个新的编译器时都会引用它。

对于使用 setjmplongjmp的 C 语言实现的更高层次视图,我推荐 Dave Hanson 的 接口和实现(如 Eli Bendersky)。