C++异常处理的额外开销

36
为什么嵌入式平台开发人员不断尝试从他们的SDK中移除C++异常的使用?
例如,Bada SDK针对异常使用提供了以下解决方法,看起来异常丑陋:
 result
 MyApp::InitTimer()
 {
    result r = E_SUCCESS;

    _pTimer = new Timer;

    r = _pTimer->Construct(*this);
    if (IsFailed(r))
    {
        goto CATCH;
    }

    _pTimer->Start(1000);
    if (IsFailed(r))
    {
        goto CATCH;
    }

    return r;
 CATCH:
     return r;
 }

这种行为的原因是什么?

据我所知,ARM编译器完全支持C++异常,这应该不是问题所在。还有其他原因吗?在ARM平台上使用异常和展开的开销真的很大吗,需要花费很多时间来进行这样的变通吗?

也许还有我没有意识到的其他原因?

谢谢。


18
+1 是用来形容它非常丑陋的... - Eran
5
一个主要原因是旧代码。除非代码从一开始就编写为异常安全,否则它就不是异常安全的。这是谷歌不使用异常的重要原因之一:没有从一开始就这样做,现在我们在某种程度上被这个决定束缚住了。 - edA-qa mort-ora-y
我建议将“usage”标签(在我看来似乎是无操作)更改为“嵌入式”。 - Dan
你是指“为什么平台不允许使用异常”还是更一般的“为什么人们不使用异常”?对于前者,禁用异常是确保与使用“嵌入式C ++”子集的平台兼容的一种途径。 http://en.wikipedia.org/wiki/Embedded_C%2B%2B - unixsmurf
在这里看到很多答案。有一个关于异常的帖子。 setjmp()longjmp() 更加受控。每个对象通常都会被输入到异常表中,而在每个文件编译中查找表是非最优的。如果它坐落在磁盘上,通常不会有问题。嵌入式应用程序通常没有磁盘。即使在今天(2013年),g ++ 的开发人员仍在尝试优化这些表。在某些情况下,它们可能与代码一样大! - artless noise
6个回答

60

只是我的个人意见...

我专门咨询嵌入式系统,大部分是硬实时和/或安全/生命至关重要的。大多数在256K的闪存/ROM或更少的存储空间中运行 - 换句话说,这些不是具有1GB + RAM / flash和1GHz + CPU的“类似于PC”的VME总线系统。它们是嵌入式的、有些资源受限制的系统。

我会说使用C++的产品中至少75%的编译器禁用异常(即使用禁用异常的编译器开关编译的代码)。我总是问原因。信不信由你,最常见的答案并不是运行时或内存开销/成本。

答案通常是以下一些组合:

  • “我们不确定如何编写异常安全的代码”。对他们来说,检查返回值更熟悉、更简单、更安全。
  • “假设您只在特殊情况下抛出异常,这些都是我们通过自己的关键错误处理程序例程重新启动的情况”
  • 旧代码问题(正如jalf所提到的)- 他们正在使用很多年前开始的代码,当时他们的编译器不支持异常,或者没有正确或高效地实现它们

此外 - 通常存在一些模糊的不确定性/担忧,但几乎总是未经量化/未经分析的,只是随意提出并被接受。我可以向您展示报告/文章,其中指出异常的开销为3%、10%-15%或~30% - 任选其一。人们往往引用符合自己观点的数字。几乎总是,这篇文章已过时,平台/工具集完全不同等等,因此如Roddy所说,你必须在你的平台上进行自己的测量。

我并不一定支持这些立场中的任何一个,我只是给您来自许多在嵌入式系统上使用C++的公司听到的现实反馈/解释,因为您的问题是“为什么许多嵌入式开发人员避免使用异常?”


2
我的想法和经验也是朝着这个方向发展的,我想补充一点,即项目中添加的任何代码/二进制文件都会增加风险。如果从代码中获得的价值无法弥补风险和需要验证该代码的qa周期,则根本不要添加它。嵌入式系统希望比桌面系统更可靠,链接、使用或未使用的每一行代码或库blob都会增加您的风险。 - old_timer

20

我能想到几个可能的原因:

  • 旧版本的编译器不支持异常,所以很多代码都是在没有使用异常的情况下编写的(也建立了相关约定)
  • 异常确实会带来一些开销,可能会占用你总执行时间的10-15%(虽然它们也可以被实现为几乎不花费时间,但是相反需要使用相当多的内存,这在嵌入式系统中可能并不理想)
  • 嵌入式程序员对代码大小、性能和复杂度有点过于谨慎。他们经常担心“高级”功能无法与他们的编译器正确配合(而且他们通常也是正确的)

12
你能提供关于那个10-15%数值的参考资料吗?或者它们占用大量内存的说法可以证明吗? - edA-qa mort-ora-y
2
@edA-qa C++性能报告:http://www2.research.att.com/~bs/performanceTR.pdf,尽管它给出了不同的数字。 - Igor Skochinsky
哪个部分包含异常处理的数字?在2.4中,它们详细说明了差异/细节,但似乎没有给出实际的比较数字。 - edA-qa mort-ora-y
@edA:请注意,我并没有说“它们总是会花费那么多时间”,只是说它可能需要那么多时间。我现在没有源代码在这里,但是是的,我见过一些基准测试显示性能会有所下降。但这显然非常取决于具体的实现方式。我不是说“异常很慢”,而是说“异常可能很慢”。至于“大量内存”的部分,这是真的,对于适当定义的“大量”而言。基于表格的方法(基本上将指令指针值映射到包含静态异常信息的表条目中)会占用空间,以避免速度损失。 - jalf
1
@KillianDS:就我们所知,它可能是由开发人员讨论异常开销而导致的“开发时间开销”。 - peterchen
显示剩余4条评论

8
我认为这些都是虚假信息。现在,异常的开销只存在于创建具有构造函数/析构函数的对象块的入口和出口处,但在大多数情况下,这确实不应该成为问题。
“先测量,后优化”。
然而,抛出异常通常比仅返回布尔标志要慢,因此仅在发生异常事件时抛出异常。
我曾经看到过一种情况,当抛出异常以进行潜在的调试时,RTL将从符号表中构建整个可打印堆栈跟踪。正如你所想象的那样,这是不好的事情。这是几年前的事情,当这一点被揭示出来时,调试库被匆忙修复了。
但是,在我看来,通过正确使用异常可以获得的可靠性远远超过了微小的性能损失。使用它们,但要小心。
编辑:
@jalf提出了一些很好的观点,我的答案针对的是为什么许多嵌入式开发人员普遍贬低异常相关问题。
但是,如果特定平台SDK的开发人员说“不要使用异常”,那么您可能必须遵循这一点。也许他们的库或编译器中有特定的异常实现问题,或者他们担心在回调中抛出异常会导致由于自己代码缺乏异常安全性而引起的问题。

4
只对异常事件抛出异常。 - anno
1
请记住,我们正在谈论嵌入式平台,在这些平台上,异常的实现可能不像在更主流的平台上那样被优化。 - jalf
1
另外,你没有回答问题。问题不是“goto是否邪恶,还是我应该使用异常”,而是“为什么许多嵌入式SDK不鼓励使用异常?” - jalf
@jalf:实际上,“问题”是“C++异常开销”,这可能并不那么有帮助:-(关于平台问题的观点很公正。不过我会进行编辑... - Roddy
先测量,后优化。 - Jonny Dee

5
这种对待异常的态度与性能或编译器支持无关,而与将异常添加到代码中会增加复杂性的想法有关。

据我所知,这个想法几乎总是一个误解,但由于某些难以理解的原因,似乎有强大的支持者


此外,Joel Spolsky(*Joel Spolsky*系列的一部分)也曾经被指“失去理智”(参见http://www.codinghorror.com/blog/2006/09/has-joel-spolsky-jumped-the-shark.html)。 - BlueRaja - Danny Pflughoeft
异常确实会增加代码的复杂性,如果复杂性是通过圈复杂度来衡量的话。当正确计算时,函数抛出的每种类型的异常都会将该函数的圈复杂度增加两个。 - David Hammen
4
你需要将异常与其他错误报告方案进行比较,而不是将异常与没有任何东西进行比较。 - n. m.

5
在其他答案中提出了"goto语句是有害的"的观点相反的意见。我将这个社区wiki,因为我知道这个相反的意见会被攻击。
任何值得称道的实时程序员都知道使用goto。这是一种广泛使用和广泛接受的处理错误的机制。许多硬实时编程环境不实现setjmp.h。异常在概念上只是受限制的setjmp和longjmp机制。那么为什么要提供异常,而底层机制却被禁止呢?
如果所有抛出的异常都可以保证本地处理,那么环境可能允许异常。问题在于,为什么要这样做?唯一的理由是goto总是有害的。好吧,它们并不总是有害的。

我想这也有一定的道理。我认识一些有才华的程序员,他们持有相同的观点,虽然我可能不同意你的看法,但是不同的意见总是有用的。 - Yippie-Ki-Yay
3
好的,异常实际上只是goto语句的一种抽象形式,但是具有自动化的RAII清理优点 :) - Roddy
@David:真的吗?哪些平台完全支持异常(这就是OP所问的),但使RAII成为不可能? - jalf
5
@David. "Realtime" 这个术语很广泛:我已经用汇编、Pascal、C、C++等语言在PIC及以上的实时系统上进行了30多年的编程。为了更轻松地完成任务,您可以使用符合您需求的语言特性。在许多情况下,RAII和模板也是其中的一部分。 - Roddy
是的,我刚和一位年轻的同事进行了长时间的讨论,讨论的内容是为什么我不应该在嵌入式系统中使用goto来处理错误。我试图向他解释异常(以及break和return)都只是涂了口红的goto。最终,重新构建我的代码比继续与他争论更容易。 - Edward Falk
显示剩余6条评论

4
现代 C++ 编译器可以将异常的运行时间开销降低到不到 3%。如果极端程序员仍然认为开销太大,他们可能会采用一些“肮脏的技巧”来规避异常。
请参阅 Bjarne Strourstrup 的页面,了解为什么要使用异常:Why use Exceptions ?

1
我并不是嵌入式平台的世界级专家,但我认为它们很少有可以被视为“现代C++编译器”的东西,这使得3%的数字相当无关紧要。 :) - jalf

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