我正在看C ++中的系统错误处理-Andrei Alexandrescu他声称C ++中的异常非常慢。
对于C ++ 98来说仍然如此吗?
我正在看C ++中的系统错误处理-Andrei Alexandrescu他声称C ++中的异常非常慢。
对于C ++ 98来说仍然如此吗?
Answers:
当今用于例外(Itanium ABI,VC ++ 64位)的主要模型是零成本模型例外。
这样做的想法是,编译器将生成一个边表,该边表将可能引发异常的任何点(程序计数器)映射到处理程序列表,而不是通过设置防护程序并在所有地方明确检查是否存在异常来浪费时间。引发异常时,将查询此列表以选择合适的处理程序(如果有),并取消堆栈堆栈。
与典型if (error)
策略相比:
if
发生异常时,费用约为10倍/ 20倍然而,成本并不是微不足道的:
dynamic_cast
对每个处理程序的测试)因此,大多数缓存未命中,因此与纯CPU代码相比并不容易。
注意:有关更多详细信息,请阅读TR18015报告的第5.4节异常处理(pdf)
因此,是的,例外在异常路径上运行缓慢,但与其他情况相比,它们通常比显式检查(if
策略)更快。
注意:安德烈·亚历山德列斯库(Andrei Alexandrescu)似乎质疑这种“快速”。我个人已经看到事情发生了双向变化,有些程序在例外情况下运行得更快,而在分支程序中运行得更快,因此在某些情况下确实确实缺乏优化性。
有关系吗 ?
我会声称没有。编写程序时应考虑可读性,而不要考虑性能(至少,不作为首要标准)。当人们期望调用者不能或不希望当场处理失败并将失败传递到堆栈时,将使用异常。奖励:在C ++ 11中,可以使用标准库在线程之间封送异常。
不过,这很微妙,我声称map::find
不应抛出,但是如果尝试取消引用失败,因为它为null,那么我可以map::find
返回一个that checked_ptr
抛出异常:在后一种情况下,例如在Alexandrescu引入的类的情况下,调用者选择在显式检查和依赖异常之间。在不给呼叫者更多责任的情况下赋予呼叫者权力通常是好的设计的标志。
abort
将允许您测量二进制大小的占用空间并检查加载时间/ i缓存的行为是否类似。当然,最好不要打任何的abort
...
提出问题后,我正准备去出租车的路上去看医生,所以我只有时间发表简短评论。但是,在评论,赞成和反对之后,我最好添加自己的答案。即使Matthieu的答案已经相当不错了。
重新提出申诉
“我当时在看C ++中的系统错误处理-Andrei Alexandrescu他声称C ++中的异常非常慢。”
如果这确实是安德烈(Andrei)声称的话,那么他有一次非常误导,甚至不是完全错误。与使用该语言的其他基本操作相比,对于引发/抛出的异常始终较慢,而与编程语言无关。如声称的声明所示,不仅是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
缓存问题(例如,较短的代码可能适合缓存)
该报告具有要考虑的不同方面的列表,但是无论如何,获取有关执行效率的事实的唯一实用方法可能是在确定的开发时间范围内并与开发人员一起使用异常而不使用异常来实现同一程序。熟悉每种方法,然后进行测量。
正确性几乎总是胜过效率。
没有例外,很容易发生以下情况:
某些代码P用于获取资源或计算某些信息。
调用代码C应该已经检查了成功/失败,但是没有成功。
C后面的代码中使用了不存在的资源或无效的信息,从而导致普遍混乱。
主要问题是要点(2),在这种情况下,使用通常的返回码方案,不会强制调用代码C进行检查。
有两种主要方法可以强制执行此类检查:
P失败时直接引发异常。
其中P返回C 使用其主值之前必须检查的对象(否则为异常或终止)。
第二种方法是AFAIK,由Barton和Nackman在他们的书《科学与工程C ++:先进技术和示例入门》中首次描述,他们引入了一个称为Fallow
“可能的”函数结果的类。optional
Boost库现在提供了一个类似的类。对于非POD结果,您可以Optional
使用自己std::vector
作为值载体轻松地实现类。
在第一种方法中,调用代码C除了使用异常处理技术外别无选择。但是,使用第二种方法,调用代码C本身可以决定是if
基于检查还是进行常规异常处理。因此,第二种方法支持在程序员与执行时间效率之间进行权衡。
“我想知道对于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语言回调的两侧使用异常处理。
longjmp
需要处理程序即可。
try..finally
构造无需堆栈展开即可实现。F#,C#和Java都实现了try..finally
不使用堆栈展开的功能。您只longjmp
需要处理程序(正如我已经解释的那样)。
这取决于编译器。
例如,GCC以处理异常时的性能很差而著称,但是在过去几年中,这种情况变得更好了。
但是请注意,处理异常(顾名思义)应该是异常,而不是软件设计中的规则。当您的应用程序每秒抛出如此多的异常以至于影响性能并且仍然被认为是正常操作时,您应该宁愿以不同的方式去做。
异常是通过清除所有笨拙的错误处理代码来提高代码可读性的好方法,但是一旦它们成为常规程序流程的一部分,它们就会变得很难遵循。请记住,a throw
几乎是goto catch
伪装的。
throw new Exception
是Java主义的。通常应该永远不要抛出指针。
是的,但这没关系。为什么?
读这个:
https //blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx
基本上说,使用像Alexandrescu所述的异常(速度减慢50倍,因为它们使用catch
as 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部门划分,那将变得更快...
就像in silico一样,它取决于实现,但是通常对于任何实现而言,异常都被认为是缓慢的,因此不应在性能密集型代码中使用。
编辑:我并不是说根本不使用它们,但是对于性能密集的代码,最好避免使用它们。