C ++中的异常真的很慢吗
Posted
技术标签:
【中文标题】C ++中的异常真的很慢吗【英文标题】:Are Exceptions in C++ really slow 【发布时间】:2012-11-29 22:11:15 【问题描述】:我在看Systematic Error Handling in C++—Andrei Alexandrescu,他声称 C++ 中的异常非常慢。
对于 C++98 仍然如此吗?
【问题讨论】:
询问“C++98 异常”是否比“C++03 异常”或“C++11 异常”快/慢是没有意义的。它们的性能取决于编译器如何在您的程序中实现它们,而 C++ 标准没有说明它们应该如何实现;唯一的要求是他们的行为必须遵循标准(“as-if”规则)。 相关(但不是真正重复)问题:***.com/questions/691168/… 是的,它很慢,但它们不应该被抛出用于正常操作或用作分支 我找到了similar question。 为了澄清 BЈовић 所说的,使用异常并不可怕。当抛出异常时,您会遇到(可能)耗时的操作。我也很好奇为什么你想特别了解 C++89 ......最新版本是 C++11,并且运行异常所需的时间是实现定义的,因此我的“潜在”耗时. 【参考方案1】:目前用于异常的主要模型(Itanium ABI、VC++ 64 位)是零成本模型异常。
这个想法是,编译器不会通过设置保护和显式检查是否存在异常来浪费时间,而是生成一个侧表,将任何可能引发异常的点(程序计数器)映射到处理程序。当抛出异常时,会参考此列表来选择正确的处理程序(如果有)并展开堆栈。
与典型的if (error)
策略相比:
if
的成本约为 10 倍/20 倍
然而,衡量成本并非易事:
边桌通常冷,因此从内存中获取它需要很长时间 确定正确的处理程序涉及 RTTI:要获取的许多 RTTI 描述符、分散在内存中以及要运行的复杂操作(基本上每个处理程序的dynamic_cast
测试)
因此,主要是缓存未命中,因此与纯 CPU 代码相比并非微不足道。
注意:更多详情,请阅读TR18015 report, chapter 5.4 Exception Handling (pdf)
所以,是的,异常在异常路径上很慢,但通常比显式检查(if
策略)要快。
注意:Andrei Alexandrescu 似乎对这个“更快”提出了质疑。我个人看到事情是双向的,一些程序在异常情况下速度更快,而另一些程序在分支情况下速度更快,因此在某些情况下似乎确实失去了可优化性。
重要吗?
我会声称它没有。编写程序时应考虑可读性,而不是性能(至少,不是作为首要标准)。当人们期望调用者不能或不希望当场处理故障并将其传递到堆栈时,将使用异常。 奖励:在 C++11 中,可以使用标准库在线程之间编组异常。
虽然这很微妙,但我声称 map::find
不应该抛出但我很好 map::find
返回一个 checked_ptr
如果尝试取消引用它失败,因为它是 null:在后一种情况下,如在 Alexandrescu 介绍的类的情况下,调用者在显式检查和依赖异常之间进行选择。在不赋予呼叫者更多责任的情况下赋予呼叫者权力通常是良好设计的标志。
【讨论】:
+1 我只会添加四件事:(0)关于在 C++11 中添加的对重新抛出的支持; (1) 参考委员会关于 c++ 效率的报告; (2)关于正确性的一些评论(甚至胜过可读性); (3) 关于性能,关于在不使用异常的情况下衡量它的评论(都是相对的) @Cheersandhth.-Alf:(0)、(1)和(3)完成:谢谢。关于正确性 (2),虽然它胜过可读性,但我不确定异常会导致比其他错误处理策略更正确的代码(很容易忘记执行异常创建的许多不可见路径)。 描述可能在本地是正确的,但值得注意的是,异常的存在对编译器可以进行的假设和优化具有全局意义。这些含义的问题是它们“没有微不足道的反例”,因为编译器总是可以看穿一个小程序。在有和没有异常的真实的大型代码库上进行分析可能是一个好主意。 > 零成本模型,顾名思义,在没有异常发生时是免费的,这实际上并不适用于最精细的细节。生成更多代码总是会对性能产生影响,即使是很小和微妙的......操作系统加载可执行文件可能需要更长的时间,否则您将获得更多的 i-cache 未命中。另外,堆栈展开代码呢?另外,你可以做一些实验来衡量效果,而不是用理性的思维去理解它? @jheriko:实际上,我相信我已经解决了你的大部分问题。加载时间不应受到影响(不应加载冷代码),i-cache 不应受到影响(冷代码不应进入 i-cache),......所以要解决一个缺失的问题: “如何测量” => 用调用abort
替换引发的任何异常将允许您测量二进制大小的占用空间并检查加载时间/i-cache 的行为是否相似。当然,最好不要打任何abort
...【参考方案2】:
除非您将代码转换为程序集或对其进行基准测试,否则您永远不能声称性能。
这是您所看到的:(quick-bench)
错误代码对出现的百分比不敏感。只要它们从未被抛出,异常就会有一点开销。一旦你扔掉它们,痛苦就开始了。在这个例子中,它在 0%、1%、10%、50% 和 90% 的情况下被抛出。当 90% 的时间抛出异常时,代码比 10% 的时间抛出异常的情况慢 8 倍。如您所见,异常非常缓慢。如果经常抛出,请勿使用它们。如果您的应用程序没有实时要求,如果它们很少发生,请随意丢弃它们。
您会看到许多关于它们的矛盾意见。但最后, 异常很慢吗?我不判断。只需观看基准测试。
【讨论】:
感谢您将这些放在一起。我用几个不同的编译器选择重新运行它,并且在每种情况下它都说异常和错误代码对于“00”情况具有相同的性能。在我的典型用例中,这是唯一需要高性能的。【参考方案3】:是的,但这没关系。 为什么? 阅读:https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx
基本上说,使用 Alexandrescu 所描述的异常(速度减慢 50 倍,因为他们使用 catch
作为 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())
//...
附:还要注意,即使异常很慢...如果您在执行期间不在该部分代码上花费大量时间,那也不是问题...例如,如果浮点除法很慢并且您将其提高了 4 倍如果你把 0.3% 的时间花在 FP 除法上也没关系...
【讨论】:
【参考方案4】:当问题发布时,我正在去看医生的路上,出租车在等着,所以我只有时间来做一个简短的评论。但是现在已经评论、赞成和反对,我最好添加我自己的答案。就算Matthieu’s answer已经不错了。
与其他语言相比,C++ 中的异常是否特别慢?
重新声明
“我在看 Systematic Error Handling in C++—Andrei Alexandrescu 他声称 C++ 中的异常非常慢。”
如果这就是安德烈所说的,那么这一次他是非常具有误导性的,如果不是完全错误的话。与语言中的其他基本操作相比,引发/抛出的异常总是很慢,不管编程语言是什么。正如所声称的那样,不仅仅是在 C++ 中,或者在 C++ 中比在其他语言中更是如此。
一般来说,无论语言如何,两个基本的语言特性都比其他语言慢几个数量级,因为它们转换为处理复杂数据结构的例程调用,是
异常抛出,以及
动态内存分配。
在 C++ 中,人们通常可以避免在时间要求严格的代码中同时避免这两种情况。
不幸的是 没有免费午餐这样的东西,即使 C++ 的默认效率非常接近。 :-) 对于通过避免异常抛出和动态内存分配而获得的效率通常是通过在较低抽象级别进行编码来实现的,将 C++ 用作“更好的 C”。较低的抽象意味着更大的“复杂性”。
更高的复杂性意味着花费更多的时间进行维护,而代码重用的好处很少或根本没有,这是真正的金钱成本,即使难以估计或衡量。即,如果需要,使用 C++ 可以用一些程序员效率换取执行效率。是否这样做在很大程度上是一个工程和直觉决定,因为在实践中,只有收益而不是成本可以很容易地估计和衡量。
有 C++ 异常抛出性能的客观衡量标准吗?
是的,国际 C++ 标准化委员会已经发布了Technical Report on C++ performance, TR18015。
异常“慢”是什么意思?
这主要意味着 throw
与例如相比可能需要很长时间™ int
分配,由于搜索处理程序。
正如 TR18015 在其第 5.4 节“异常”中所讨论的,有两种主要的异常处理实现策略,
每个try
-block 动态设置异常捕获的方法,以便在抛出异常时执行动态处理程序链的搜索,并且
编译器生成静态查找表的方法,用于确定引发异常的处理程序。
第一种非常灵活和通用的方法几乎在 32 位 Windows 中被强制使用,而在 64 位域和 *nix 域中通常使用第二种效率更高的方法。
正如该报告所讨论的,对于每种方法,异常处理影响效率的三个主要领域:
try
-blocks,
常规函数(优化机会),以及
throw
-表达式。
主要是,使用动态处理程序方法(32 位 Windows)异常处理会对 try
块产生影响,主要与语言无关(因为这是由 Windows 的 结构化异常处理 方案强制执行的),而静态表方法对于try
-blocks 的成本大致为零。与 SO 答案相比,讨论这一点需要更多的空间和研究。因此,请参阅报告了解详情。
不幸的是,从 2006 年开始,到 2012 年底,这份报告已经有点过时了,据我所知,没有任何可比的更新。
另一个重要的观点是,使用异常对性能的影响与支持语言功能的孤立效率有很大不同,因为正如报告所述,
“在考虑异常处理时,必须将其与其他处理方式进行对比 处理错误。”
例如:
不同编程风格导致的维护成本(正确性)
冗余呼叫站点 if
故障检查与集中式 try
缓存问题(例如较短的代码可能适合缓存)
报告有不同的方面需要考虑,但无论如何,获得关于执行效率的确凿事实的唯一实用方法可能是在确定的开发时间上限内使用异常而不使用异常来实现相同的程序,与熟悉每种方式的开发人员一起,然后MEASURE。
什么是避免异常开销的好方法?
正确性几乎总是胜过效率。
无一例外,很容易发生以下情况:
某些代码 P 用于获取资源或计算某些信息。
调用代码 C 应该检查成功/失败,但没有。
在 C 之后的代码中使用了不存在的资源或无效信息,造成了一般性混乱。
主要问题是第 (2) 点,在通常的 返回码 方案中,调用代码 C 不会被强制检查。
有两种主要方法可以强制进行此类检查:
其中P失败时直接抛出异常。
其中 P 返回一个对象,C 在使用其主值之前必须检查(否则为异常或终止)。
第二种方法是 AFAIK,首先由 Barton 和 Nackman 在他们的书 *Scientific and Engineering C++: An Introduction with Advanced Techniques and Examples 中进行了描述,他们在其中引入了一个名为 Fallow
的类以获得“可能的”函数结果。 Boost 库现在提供了一个名为 optional
的类似类。并且您可以自己轻松实现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 语言回调的两侧使用。
【讨论】:
“与该语言中的其他基本操作相比,异常总是很慢,无论编程语言如何”...除了旨在将异常使用编译成普通流控制的语言。 “抛出异常涉及分配和堆栈展开”。一般来说,这显然也不正确,OCaml 也是一个反例。在垃圾收集语言中,不需要展开堆栈,因为没有析构函数,因此您只需longjmp
处理程序即可。
@JonHarrop:大概你不知道 Pyhon 有一个用于异常处理的 finally 子句。这意味着 Python 实现要么具有堆栈展开,要么不是 Python。您似乎完全不知道您提出(幻想)声称的主题。对不起。
@Cheersandhth.-Alf:“Pyhon 有一个用于异常处理的 finally 子句。这意味着 Python 实现要么具有堆栈展开,要么不是 Python”。 try..finally
构造可以在不展开堆栈的情况下实现。 F#、C# 和 Java 都在不使用堆栈展开的情况下实现 try..finally
。你只需 longjmp
给处理程序(正如我已经解释过的)。
@JonHarrop:你听起来喜欢摆出两难境地。但它与到目前为止所讨论的任何事情都没有关联,到目前为止,您已经发布了一长串听起来负面的废话。我必须相信你才能同意或不同意一些模糊的措辞,因为作为对手,你选择的是你将揭示它“意味着”什么,而且我当然不毕竟信任你那些毫无意义的废话,投反对票等。【参考方案5】:
正如 in silico 所说,它依赖于实现,但一般来说,任何实现都认为异常很慢,不应在性能密集型代码中使用。
编辑:我并不是说根本不要使用它们,但对于性能密集型代码,最好避免使用它们。
【讨论】:
这充其量是查看异常性能的一种非常简单的方法。例如,GCC 使用“零成本”实现,如果没有抛出异常,您不会招致性能损失。并且例外适用于例外(即罕见)情况,因此即使它们在某些指标上很慢,但仍然没有足够的理由不使用它们。 @insilico 如果你看看我为什么说,我没有说不要使用异常句号。我指定了性能密集型代码,这是一个准确的评估,我主要使用 gpgpus 工作,如果我使用异常,我会被枪杀。【参考方案6】:这取决于编译器。
例如,GCC 以在处理异常时性能非常差而闻名,但在过去几年中,这种情况变得相当好。
但请注意,处理异常应该——正如其名称所说——是软件设计中的异常而不是规则。当您的应用程序每秒抛出如此多的异常以影响性能并且这仍然被认为是正常操作时,您应该考虑以不同的方式做事。
异常是一种很好的方法,可以通过消除所有笨拙的错误处理代码来提高代码的可读性,但是一旦它们成为正常程序流程的一部分,它们就会变得很难遵循。请记住,throw
几乎是伪装的goto catch
。
【讨论】:
-1 是现在的问题,“这对于 C++98 是否仍然适用”,这当然不依赖于编译器。另外,这个答案的throw new Exception
是Java 主义。作为一项规则,我们应该从不抛出指针。
98 标准是否明确规定了如何实现异常?
C++98 是 ISO 标准,而不是编译器。实现它的编译器有很多。
@thecoshman:不。C++ 标准没有说明应该如何实现任何东西(标准的“实施限制”部分可能除外)。以上是关于C ++中的异常真的很慢吗的主要内容,如果未能解决你的问题,请参考以下文章