113

我在看C++ 中的系统错误处理——Andrei Alexandrescu他声称 C++ 中的异常非常非常慢。

这对于 C++98 仍然适用吗?

4

6 回答 6

184

今天用于异常的主要模型(Itanium ABI,VC++ 64 位)是零成本模型异常。

这个想法是,编译器不会通过设置警卫和显式检查是否存在异常来浪费时间,而是生成一个侧表,将任何可能引发异常的点(程序计数器)映射到处理程序列表。当抛出异常时,将参考此列表以选择正确的处理程序(如果有)并展开堆栈。

与典型if (error)策略相比:

  • 顾名思义,零成本模型在没有异常发生时是免费的
  • if发生异常时,它的成本约为 10 倍/20 倍

然而,成本并不是微不足道的:

  • 边桌通常很冷,因此从内存中获取它需要很长时间
  • 确定正确的处理程序涉及 RTTI:要获取的许多 RTTI 描述符,分散在内存中,以及要运行的复杂操作(基本上是dynamic_cast对每个处理程序的测试)

因此,主要是缓存未命中,因此与纯 CPU 代码相比并非微不足道。

注意:有关更多详细信息,请阅读TR18015 报告,第 5.4 章异常处理 (pdf)

所以,是的,异常路径上的异常很慢,但它们通常比显式检查(if策略)更快。

注意:Andrei Alexandrescu 似乎对这个“更快”提出了质疑。我个人看到事情是双向的,有些程序在异常情况下更快,而另一些程序在分支上更快,因此在某些情况下似乎确实失去了可优化性。


有关系吗 ?

我会声称它没有。编写程序时应该考虑到可读性,而不是性能(至少,不是作为首要标准)。当人们期望调用者不能或不希望当场处理故障并将其传递到堆栈时,将使用异常。奖励:在 C++11 中,可以使用标准库在线程之间编组异常。

虽然这很微妙,但我声称不map::find应该抛出,但如果尝试取消引用它失败,我可以map::find返回checked_ptr抛出,因为它是 null:在后一种情况下,就像 Alexandrescu 引入的类一样,调用者选择在显式检查和依赖异常之间。在不赋予呼叫者更多责任的情况下赋予呼叫者权力通常是良好设计的标志。

于 2012-12-12T09:11:27.770 回答
65

当问题发布时,我正在去看医生的路上,出租车在等着,所以我只有时间来做一个简短的评论。但是现在已经评论和赞成和反对,我最好添加我自己的答案。即使马蒂厄的回答已经很不错了。


与其他语言相比,C++ 中的异常是否特别慢?

重新索赔

“我在看C++ 中的系统错误处理——Andrei Alexandrescu他声称 C++ 中的异常非常慢。”

如果这确实是安德烈所声称的,那么这一次他是非常具有误导性的,如果不是完全错误的话。对于引发/抛出的异常,与该语言中的其他基本操作相比总是很慢,无论编程语言如何。正如声称的声明所表明的那样,不仅在 C++ 中,或者在 C++ 中比在其他语言中更是如此。

一般来说,无论语言如何,两个基本语言特性比其他语言慢几个数量级,因为它们转换为处理复杂数据结构的例程调用,是

  • 异常抛出,和

  • 动态内存分配。

令人高兴的是,在 C++ 中,人们通常可以在时间关键的代码中避免这两种情况。

不幸的是,没有免费午餐这样的东西,即使 C++ 的默认效率非常接近。:-) 对于通过避免异常抛出和动态内存分配而获得的效率通常是通过在较低抽象级别进行编码来实现的,将 C++ 用作“更好的 C”。较低的抽象意味着更大的“复杂性”。

更大的复杂性意味着更多的时间花在维护上,而代码重用的好处很少或没有,这是真正的金钱成本,即使难以估计或衡量。即,如果需要,使用 C++ 可以用一些程序员效率换取执行效率。是否这样做在很大程度上是一个工程和直觉决定,因为在实践中,只有收益,而不是成本,可以很容易地估计和衡量。


是否有 C++ 异常抛出性能的客观衡量标准?

是的,国际 C++ 标准化委员会发布了一份关于 C++ 性能的技术报告,TR18015


异常“慢”是什么意思?

这主要意味着,由于搜索处理程序,与分配throw相比,a 可能需要很长时间。int

正如 TR18015 在其第 5.4 节“异常”中所讨论的,有两种主要的异常处理实现策略,

  • 每个try块动态设置异常捕获的方法,以便在抛出异常时执行处理程序的动态链的搜索,以及

  • 编译器生成静态查找表的方法,这些查找表用于确定抛出异常的处理程序。

第一种非常灵活和通用的方法几乎在 32 位 Windows 中强制使用,而在 64 位域和 *nix 域中,通常使用第二种更有效的方法。

此外,正如该报告所讨论的,对于每种方法,异常处理都会影响效率的三个主要领域:

  • try-块,

  • 常规功能(优化机会),以及

  • throw-表达式。

主要是,动态处理程序方法(32 位 Windows)异常处理对try块有影响,主要与语言无关(因为这是由 Windows 的结构化异常处理方案强制执行的),而静态表方法的成本大致为零try-块。与 SO 答案相比,讨论这一点需要更多的空间和研究。因此,请参阅报告了解详情。

不幸的是,从 2006 年开始,到 2012 年底,这份报告已经有点过时了,据我所知,没有任何可比的更新。

另一个重要的观点是,使用异常对性能的影响与支持语言功能的孤立效率有很大不同,因为正如报告所指出的,

“在考虑异常处理时,必须将其与处理错误的替代方法进行对比。”

例如:

  • 不同编程风格导致的维护成本(正确性)

  • 冗余呼叫站点if故障检查与集中式try

  • 缓存问题(例如较短的代码可能适合缓存)

该报告有不同的方面需要考虑,但无论如何,获得关于执行效率的确凿事实的唯一实用方法可能是在确定的开发时间上限内,并与开发人员一起使用异常而不使用异常来实现相同的程序熟悉每种方式,然后MEASURE


什么是避免异常开销的好方法?

正确性几乎总是胜过效率。

没有例外,以下情况很容易发生:

  1. 某些代码 P 旨在获取资源或计算某些信息。

  2. 调用代码 C 应该检查成功/失败,但没有。

  3. 在 C 之后的代码中使用了不存在的资源或无效信息,从而导致普遍的混乱。

主要问题是第 (2) 点,在通常的返回代码方案中,调用代码 C 不会被强制检查。

有两种主要方法可以强制进行此类检查:

  • 其中P失败时直接抛出异常。

  • 其中 P 返回一个对象,C 在使用其主值之前必须检查该对象(否则为异常或终止)。

第二种方法是 AFAIK,首先由 Barton 和 Nackman 在他们的书 * Scientific and Engineering C++: An Introduction with Advanced Techniques and Examples中描述,他们在其中引入了一个名为Fallow“可能”函数结果的类。optionalBoost 库现在提供了一个类似的类。并且您可以轻松地Optional自己实现一个类,std::vector在非 POD 结果的情况下使用作为值载体。

对于第一种方法,调用代码 C 没有选择,只能使用异常处理技术。然而,使用第二种方法,调用代码 C 可以自己决定是进行if基于检查还是进行一般异常处理。因此,第二种方法支持在程序员与执行时间效率之间进行权衡。


各种 C++ 标准对异常性能有何影响?

“我想知道这对于 C++98 是否仍然适用”

C++98 是第一个 C++ 标准。对于异常,它引入了异常类的标准层次结构(不幸的是相当不完善)。对性能的主要影响是异常规范(在 C++11 中被删除)的可能性,但是它从未被主要的 Windows C++ 编译器完全实现 Visual C++:Visual C++ 接受 C++98 异常规范语法,但只是忽略异常规范。

C++03 只是 C++98 的技术勘误。C++03 中唯一真正的新功能是值初始化。这与异常无关。

随着 C++11 标准的一般异常规范被删除,并替换为noexcept关键字。

C++11 标准还增加了对存储和重新抛出异常的支持,这对于在 C 语言回调中传播 C++ 异常非常有用。这种支持有效地限制了当前异常的存储方式。但是,据我所知,这不会影响性能,除非在较新的代码中,异常处理可能更容易在 C 语言回调的两侧使用。

于 2012-12-12T12:49:27.720 回答
18

除非您将代码转换为程序集或对其进行基准测试,否则您永远不能声称性能。

这是您所看到的:(快速工作台)

错误代码对出现的百分比不敏感。只要它们从未被抛出,异常就会有一点开销。一旦你扔掉它们,痛苦就开始了。在这个例子中,它在 0%、1%、10%、50% 和 90% 的情况下被抛出。当 90% 的时间抛出异常时,代码比 10% 的时间抛出异常的情况慢 8 倍。如您所见,异常非常缓慢。如果经常抛出,请勿使用它们。如果您的应用程序没有实时要求,如果它们很少发生,请随意丢弃它们。

你会看到很多关于他们的矛盾意见。但最后,异常是否缓慢?我不评判。只看基准。

C++ 异常性能基准

于 2018-09-26T08:53:59.967 回答
12

这取决于编译器。

例如,GCC 以在处理异常时性能非常差而闻名,但在过去几年中,这种情况变得相当好。

但请注意,处理异常应该——正如其名称所说——是软件设计中的例外而不是规则。当您的应用程序每秒抛出如此多的异常以影响性能并且这仍然被认为是正常操作时,那么您应该考虑以不同的方式做事。

异常是一种很好的方法,可以通过消除所有笨拙的错误处理代码来使代码更具可读性,但是一旦它们成为正常程序流程的一部分,它们就变得非常难以遵循。请记住,athrow几乎是goto catch变相的 a。

于 2012-12-12T08:52:21.310 回答
3

是的,但这没关系。为什么?
阅读:
https ://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx

基本上这说明使用 Alexandrescu 所描述的异常(50 倍减速,因为它们使用catchas else)是错误的。对于喜欢这样做的人来说,我希望 C++22 :) 会添加如下内容:(
请注意,这必须是核心语言,因为它基本上是编译器从现有语言生成代码)

result = attempt<lexical_cast<int>>("12345");  //lexical_cast is boost function, 'attempt'
//... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)...     
//... By default std::exception is replaced, ofc precise configuration is possible
if (result)
{
     int x = result.get(); // or result.result;
}
else 
{
     // even possible to see what is the exception that would have happened in original function
     switch (result.exception_type())
     //...

}

PS还请注意,即使异常很慢...如果您在执行期间不在该部分代码上花费大量时间,那也不是问题...例如,如果浮点除法很慢并且您将其设为4x如果您花费 0.3% 的时间进行 FP 除法,那么更快也没关系...

于 2012-12-18T11:33:52.183 回答
-1

就像 in silico 所说的它的实现依赖,但一般来说,任何实现都认为异常很慢,不应该在性能密集型代码中使用。

编辑:我并不是说根本不使用它们,但对于性能密集型代码,最好避免使用它们。

于 2012-12-12T08:46:57.527 回答