C ++中的异常真的很慢吗


98

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

对于C ++ 98来说仍然如此吗?


42
问“ C ++ 98异常”比“ C ++ 03异常”或“ C ++ 11异常”快/慢是没有道理的。它们的性能取决于编译器如何在您的程序中实现它们,而C ++标准没有说明应如何实现它们。唯一的要求是他们的行为必须遵循标准(“假设”规则)。
在计算机上

相关(但不是真的一式两份)问题:stackoverflow.com/questions/691168/...
菲利普

2
是的,这是非常缓慢的,但他们不应该抛出一个正常操作或用作分支
BЈовић

我发现了一个类似的问题
PaperBirdMaster 2012年

要弄清BЈовић所说的话,使用异常并不令人害怕。在抛出异常时,您会遇到(可能)耗时的操作。我也很好奇您为什么要特别了解C ++ 89 ...最新版本是C ++ 11,并且运行异常所需的时间是实现的定义,因此我的``潜在''耗时。
thecoshman

Answers:


162

当今用于例外(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不应抛出,但是如果尝试取消引用失败,因为它为null,那么我可以map::find返回一个that checked_ptr抛出异常:在后一种情况下,例如在Alexandrescu引入的类的情况下,调用者选择在显式检查和依赖异常之间。在不给呼叫者更多责任的情况下赋予呼叫者权力通常是好的设计的标志。


3
+1我只会添加四件事:(0)关于C ++ 11中添加的对重新投掷的支持;(1)引用委员会关于c ++效率的报告;(2)关于正确性的一些评论(甚至胜过可读性);(3)关于性能,有关在不使用异常(都是相对的)的情况下对其进行衡量的评论
干杯和hth。-Alf 2012年

2
@ Cheersandhth.-Alf:(0),(1)和(3)完成了:谢谢。关于正确性(2),尽管它胜过了可读性,但我不确定异常会导致比其他错误处理策略更正确的代码(很容易忘记执行异常创建的许多不可见路径)。
Matthieu M.

2
该描述可能是局部正确的,但可能值得注意的是,异常的存在对编译器可以进行的假设和优化具有全局意义。这些隐含的问题是它们没有“琐碎的反例”,因为编译器始终可以看到一个小程序。在有或没有例外的情况下,在现实的大型代码库上进行概要分析可能是一个好主意。
Kerrek SB 2015年

4
>顾名思义,零成本模型在没有例外的情况下是免费的,实际上这在最精细的层次上都是不正确的。生成更多代码始终会对性能产生影响,即使它很小而又微妙……OS可能需要稍长一点的时间才能加载可执行文件,否则您会遇到更多的i缓存未命中的情况。另外,堆栈展开代码又如何呢?另外,您可以做些用来衡量效果的实验,而不是试图用理性的思维来理解它?
jheriko

2
@jheriko:实际上,我相信我已经解决了您的大多数问题。加载时间不应受到影响(不应加载冷代码),不应影响i高速缓存(不应将其放入i高速缓存中),因此解决一个遗漏的问题: “如何测量” =>用调用替换所有抛出的异常,abort将允许您测量二进制大小的占用空间并检查加载时间/ i缓存的行为是否类似。当然,最好不要打任何的abort...
马修M.

60

提出问题后,我正准备去出租车的路上去看医生,所以我只有时间发表简短评论。但是,在评论,赞成和反对之后,我最好添加自己的答案。即使Matthieu的答案已经相当不错了。


与其他语言相比,C ++中的异常异常慢吗?

重新提出申诉

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

如果这确实是安德烈(Andrei)声称的话,那么他有一次非常误导,甚至不是完全错误。与使用该语言的其他基本操作相比,对于引发/抛出的异常始终较慢,而与编程语言无关。如声称的声明所示,不仅是C ++,还是C ++中的语言比其他语言更多。

通常,大多数情况下,无论哪种语言,这两种基本语言功能都比其他功能慢几个数量级,因为它们会转换为处理复杂数据结构的例程的调用。

  • 抛出异常,以及

  • 动态内存分配。

幸运的是,在C ++中,人们通常可以避免使用时间紧迫的代码。

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

更高的复杂度意味着要花更多的时间进行维护,而从代码重用中获得的收益很少甚至没有,这是真实的金钱成本,即使很难估计或衡量。即,如果需要的话,可以使用C ++将某些程序员的效率换成执行效率。是否这样做在很大程度上是一项工程和直觉决定,因为在实践中,仅收益而不是成本可以轻松估算和衡量。


是否有任何客观的C ++异常抛出性能度量?

是的,国际C ++标准化委员会已发布有关C ++性能技术报告TR18015


异常“缓慢” 意味着什么?

主要是由于搜索处理程序,throw与例如int分配相比,a 可以花费Very Long Time™ 。

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

  • 每个try-block动态设置异常捕获的方法,以便在引发异常时在处理程序的动态链中进行搜索,并且

  • 编译器生成静态查找表的方法,该表用于确定引发异常的处理程序。

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

就像该报告所讨论的那样,对于每种方法,异常处理会影响效率的三个主要方面:

  • try

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

  • throw-表情。

主要是,使用动态处理程序方法(32位Windows)时,异常处理会对try块产生影响,主要与语言无关(因为这是Windows的结构化异常处理方案所强制执行的),而静态表方法的成本大约为零try-块。讨论此问题将比SO答案实际花费更多的空间和研究。因此,请参阅该报告以获取详细信息。

不幸的是,这份2006年的报告到2012年末已经有些过时了,据我所知,没有可比的新报告。

另一个重要的观点是,使用例外对性能的影响与支持语言功能的单独效率有很大不同,因为如报告所述,

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

例如:

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

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

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

该报告具有要考虑的不同方面的列表,但是无论如何,获取有关执行效率的事实的唯一实用方法可能是在确定的开发时间范围内并与开发人员一起使用异常而不使用异常来实现同一程序。熟悉每种方法,然后进行测量


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

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

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

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

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

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

主要问题是要点(2),在这种情况下,使用通常的返回码方案,不会强制调用代码C进行检查。

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

  • P失败时直接引发异常。

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

第二种方法是AFAIK,由Barton和Nackman在他们的书《科学与工程C ++:先进技术和示例入门》中首次描述,他们引入了一个称为Fallow“可能的”函数结果的类。optionalBoost库现在提供了一个类似的类。对于非POD结果,您可以Optional使用自己std::vector作为值载体轻松地实现类。

在第一种方法中,调用代码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语言回调的两侧使用异常处理。


6
“与使用该语言的其他基本操作相比,无论使用哪种编程语言,异常总是很慢”。。。。
Ben Voigt 2012年

4
“引发异常涉及分配和堆栈展开”。通常,这显然也不是正确的,而且,OCaml也是一个反例。在垃圾回收的语言中,由于没有析构函数,因此无需展开堆栈,因此您只longjmp需要处理程序即可。
JD

2
@JonHarrop:大概您不知道Pyhon拥有处理异常的最终条款。这意味着Python实现要么具有堆栈展开功能,要么不是Python。您似乎完全不了解(幻想)声称的主题。抱歉。
干杯和健康。-Alf 2014年

2
@ Cheersandhth.-Alf:“ Pyhon拥有处理异常的最终条款。这意味着Python实现要么具有堆栈展开功能,要么不是Python”。该try..finally构造无需堆栈展开即可实现。F#,C#和Java都实现了try..finally不使用堆栈展开的功能。您只longjmp需要处理程序(正如我已经解释的那样)。
JD

4
@JonHarrop:您听起来像是在陷入困境。到目前为止,我所看到的任何内容都与之无关,到目前为止,您已经发布了一系列负面的废话。我必须信任您,以便同意或不同意一些模糊的措辞,因为作为对手,您正在选择要表明它“ 有意义”的内容,而我当然相信您,因为这些毫无意义的废话,低估等等。
干杯和健康。-Alf 2014年

13

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

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

错误代码对出现的百分比不敏感。只要不抛出异常,它们就会产生一些开销。一旦扔掉它们,苦难就开始了。在此示例中,将为0%,1%,10%,50%和90%的情况抛出该事件。当90%的时间抛出异常时,代码比10%的时间抛出异常的速度慢8倍。如您所见,异常确实很慢。如果经常扔它们,请勿使用它们。如果您的应用程序没有实时性要求,请在很少发生的情况下随意抛出它们。

您会看到许多关于它们的矛盾意见。但最后,例外情况是否缓慢?我不判断。只是看基准。

C ++例外性能基准


12

这取决于编译器。

例如,GCC以处理异常时的性能很差而著称,但是在过去几年中,这种情况变得更好了。

但是请注意,处理异常(顾名思义)应该是异常,而不是软件设计中的规则。当您的应用程序每秒抛出如此多的异常以至于影响性能并且仍然被认为是正常操作时,您应该宁愿以不同的方式去做。

异常是通过清除所有笨拙的错误处理代码来提高代码可读性的好方法,但是一旦它们成为常规程序流程的一部分,它们就会变得很难遵循。请记住,a throw几乎是goto catch伪装的。


-1代表了现在的问题,“对于C ++ 98还是如此”,这当然不依赖于编译器。同样,这个答案throw new Exception是Java主义的。通常应该永远不要抛出指针。
干杯和健康。-Alf 2012年

1
98标准是否明确规定了异常的实现方式?
thecoshman 2012年

6
C ++ 98是ISO标准,而不是编译器。有许多实现它的编译器。
菲利普

3
@thecoshman:否。C++标准未说明应如何实现任何内容(标准的“实施限制”部分可能会例外)。
计算机上的

2
@Insilico然后,我只能得出一个逻辑结论,即(令人震惊地)它是实现定义的(读取的,特定于编译器的)异常的执行方式。
thecoshman 2012年

4

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

基本上说,使用像Alexandrescu所述的异常(速度减慢50倍,因为它们使用catchas else)是错误的。话虽如此,我想对希望像C ++ 22这样做的ppl表示感谢:(
请注意,这必须是核心语言,因为它基本上是编译器根据现有代码生成代码)

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还注意到,即使异常情况如此缓慢,如果您在执行过程中没有在代码的那部分花费很多时间,这也不是问题...例如,如果float除法很慢并且将其设为4x如果您花费0.3%的时间进行FP部门划分,那将变得更快...


0

就像in silico一样,它取决于实现,但是通常对于任何实现而言,异常都被认为是缓慢的,因此不应在性能密集型代码中使用。

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


9
充其量这是一种查看异常性能的非常简单的方法。例如,GCC使用“零成本”实现,在这种情况下,如果没有引发异常,则不会对性能造成任何影响。而且,异常是针对异常(即罕见)情况的,因此,即使它们以某种度量标准变慢,这仍然是不使用它们的充分理由。
计算机上的

@insilico如果您看看我为什么这么说,我并不是说不使用异常句号。我指定了性能密集的代码,这是一个准确的评估,我主要使用gpgpus进行工作,如果使用异常,我会被枪杀。
克里斯·麦凯布
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.