所以,我知道try / catch会增加一些开销,因此不是控制流程流的好方法,但这种开销来自何处以及它的实际影响是什么?
答案 0 :(得分:93)
要点三点:
首先,在代码中实际使用try-catch块几乎没有或没有性能损失。在尝试避免将它们放入您的应用程序时,这不应该是一个考虑因素。只有在抛出异常时,性能才会发挥作用。
当除了其他人提到的堆栈展开操作等之外还抛出异常时,你应该知道发生了一大堆运行时/反射相关的东西,以便填充异常的成员诸如堆栈跟踪对象和各种类型成员等的类。
我认为这就是为什么如果要重新抛出异常的一般建议只是throw;
而不是再次抛出异常或构造一个新异常的原因之一所有这些堆栈信息都会被重新收集,而在简单投掷中它将被保留。
答案 1 :(得分:48)
我不是语言实现方面的专家(所以请稍等一下),但我认为最大的成本之一是展开堆栈并将其存储为堆栈跟踪。我怀疑只有当抛出异常时才会发生这种情况(但我不知道),如果是这样的话,每次抛出异常时这都会有相当大的隐藏成本......所以它不像你只是从一个地方跳过在另一个代码中,有很多事情要发生。
只要您使用EXCEPTIONAL行为的异常(因此不是典型的,预期的程序路径),我认为这不是问题。
答案 2 :(得分:18)
您是否在询问未抛出异常时使用try / catch / finally的开销,或者使用异常来控制流程的开销?后者有点类似于使用一根炸药点燃幼儿的生日蜡烛,相关的开销分为以下几个方面:
由于抛出的异常访问非常驻代码和数据通常不在应用程序的工作集中,因此可能会出现其他页面错误。
上述两个项目通常都会访问“冷”代码和数据,因此如果您有内存压力,则很可能出现硬页面错误:
至于成本的实际影响,这可能会有很大差异,具体取决于您当时代码中的其他内容。 Jon Skeet有一个good summary here,有一些有用的链接。我倾向于同意他的说法,即如果你达到了异常会严重影响你的表现的程度,那么除了表现之外,你在使用例外方面存在问题。
答案 3 :(得分:6)
根据我的经验,最大的开销是实际抛出异常并处理它。我曾经在一个项目中工作,其中使用类似于以下的代码来检查某人是否有权编辑某个对象。这个HasRight()方法在表示层的每个地方都使用,并且通常被称为100个对象。
bool HasRight(string rightName, DomainObject obj) {
try {
CheckRight(rightName, obj);
return true;
}
catch (Exception ex) {
return false;
}
}
void CheckRight(string rightName, DomainObject obj) {
if (!_user.Rights.Contains(rightName))
throw new Exception();
}
当测试数据库充分利用测试数据时,这会在打开新表单等时导致非常明显的减速。
所以我将它重构为以下内容,根据后来的快速测量结果,它快了大约2个数量级:
bool HasRight(string rightName, DomainObject obj) {
return _user.Rights.Contains(rightName);
}
void CheckRight(string rightName, DomainObject obj) {
if (!HasRight(rightName, obj))
throw new Exception();
}
因此,简而言之,在正常流程中使用异常比使用类似的流程流程慢两个数量级,没有例外。
答案 4 :(得分:3)
更不用说它是否在一个经常被调用的方法中,它可能会影响应用程序的整体行为。
例如,我认为在大多数情况下使用Int32.Parse是一种不好的做法,因为它会抛出一些容易被捕获的异常。
所以写下这里写的所有内容:
1)使用try..catch块来捕获意外错误 - 几乎没有性能损失。
2)如果可以避免,请不要对例外错误使用例外。
答案 5 :(得分:3)
我前一段时间写了一篇关于此事的文章,因为当时有很多人都在询问这个问题。您可以在http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx找到它和测试代码。
结果是try / catch块的开销很小,但是很小,应该被忽略。但是,如果在执行数百次的循环中运行try / catch块,则可能需要考虑将块移动到循环外部(如果可能)。
try / catch块的关键性能问题是当您实际捕获异常时。这可能会给您的应用程序带来明显的延迟。当然,当出现问题时,大多数开发人员(以及许多用户)都认为暂停是一个即将发生的例外!这里的关键是不要对正常操作使用异常处理。顾名思义,它们非常特殊,您应该尽一切可能避免它们被抛出。您不应将它们用作正常运行的程序的预期流程的一部分。
答案 6 :(得分:2)
我去年就此主题提出了blog entry。 看看这个。最重要的是,如果没有异常发生,尝试块几乎没有成本 - 而在我的笔记本电脑上,例外大约是36μs。这可能比你预期的要少,但要记住那些结果在浅堆栈上。此外,第一个例外真的很慢。
答案 7 :(得分:2)
编写,调试和维护没有编译器错误消息,代码分析警告消息和例程接受的异常(特别是在一个地方抛出并在另一个地方接受的异常)的代码要容易得多。因为它更容易,所以代码平均可以更好地编写并减少错误。
对我来说,程序员和质量开销是反对使用try-catch进行流程的主要理由。
相比之下,异常的计算机开销是微不足道的,并且在应用程序满足实际性能要求的能力方面通常很小。
答案 8 :(得分:2)
与通常接受的理论相反,try
/ catch
可能会产生重大的性能影响,并且是否会抛出异常!
多年来,Microsoft MVP在几篇博客文章中介绍了前者,我相信您可以轻松找到它们但StackOverflow非常关心关于内容的 因此,我将提供一些链接作为 filler 证据:
try
/catch
/finally
(and part two),Peter Ritchie探讨了try
/ catch
/ finally
禁用的优化问题(我还会进一步说明)用标准引用来引入这个Parse
vs. TryParse
vs. ConvertTo
公然表示"异常处理非常缓慢"并通过Int.Parse
和Int.TryParse
相互对抗来证明这一点...对于坚持TryParse
在幕后使用try
/ catch
的任何人,应该有所帮助!还有this answer,它显示了使用try
/ catch
的反汇编代码之间的区别。
似乎很明显,是在代码生成中明显可观察到的开销,而且开销甚至似乎被微软重视的人所承认!但我是,重复互联网 ......
是的,对于一条简单的代码行,有许多额外的MSIL指令,甚至不能覆盖禁用的优化,因此从技术上来说它是一种微优化。
我在几年前发布了一个答案,因为它专注于程序员的工作效率(宏观优化)而被删除。
这是不幸的,因为在这里和那里没有节省几纳秒的CPU时间可能弥补人类手动优化的许多累积小时数。你的老板支付更多费用:一小时的时间,或一小时的电脑运行?我们在什么时候拔掉插头并承认是时候购买更快的电脑?
显然,我们应该优化我们的优先级,而不仅仅是我们的代码!在我的上一个回答中,我借鉴了两段代码之间的差异。
使用try
/ catch
:
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
未使用try
/ catch
:
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
从维护开发人员的角度考虑,更有可能浪费你的时间,如果不是在分析/优化(如上所述)中,如果不是这样的话,那么甚至可能是不必要的。 try
/ catch
问题,然后滚动浏览源代码......其中一个有额外的四行样板垃圾!
随着越来越多的字段被引入类中,所有这些样板垃圾都会在源代码和反汇编代码中累积,远远超出合理的水平。每个领域有四条额外的线条,它们总是相同的线条......我们没有被教导避免重复自己吗?我想我们可以隐藏一些自制抽象背后的try
/ catch
,但是......那么我们也可以避免异常(即使用Int.TryParse
)。
这甚至不是一个复杂的例子;我已经看到尝试在try
/ catch
中实例化新课程。考虑到构造函数内部的所有代码可能会被取消某些优化的资格,否则这些优化将由编译器自动应用。有什么更好的方法可以产生编译器慢的理论,而不是编译器完全按照它所做的那样做?
假设所述构造函数抛出异常,并且因此触发了一些错误,那么糟糕的维护开发人员就必须追踪它。这可能不是一件容易的事,因为与 goto 噩梦的意大利面条代码不同,try
/ catch
会导致三维中的混乱 ,因为它可以向上移动堆栈不仅仅是同一方法的其他部分,还有其他类和方法,所有这些都将由维护开发人员观察,艰难的方式!然而,我们被告知" goto是危险的",嘿!
最后我提到,try
/ catch
有其好处,它旨在禁用优化!如果你愿意,那就是调试辅助工具!这就是它的设计目标,它应该被用作......
我认为这也是一个积极的观点。它可用于禁用优化,否则这些优化可能会破坏多线程应用程序的安全,合理的消息传递算法,并捕获可能的竞争条件;)这是我能想到使用try / catch的唯一场景。即便如此也有其他选择。
try
,catch
和finally
禁用哪些优化措施?
A.K.A
try
,catch
和finally
如何用作调试辅助工具?
12.3.3.13尝试捕获语句
对于表单 stmt 的声明:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n
- try-block 开头的 v 的明确赋值状态与 v 的明确赋值状态相同of stmt 。
- catch-block-i (对于任何 i )开头的 v 的明确赋值状态与确定的相同 stmt 开头的 v 的赋值状态。
- stmt 的终点处的 v 的明确分配状态是明确分配的,如果(且仅当) v 明确分配在 try-block 的终点和每个 catch-block-i (对于每个 i 从1到 n )。
换句话说,在每个try
语句的开头:
try
语句之前对可见对象进行的所有赋值都必须完成,这需要一个线程锁定才能启动,这对调试竞争条件很有用!try
语句类似的故事适用于每个catch
声明;假设你的try
语句(或它调用的构造函数或函数等)你分配给那个无意义的变量(让我们说, garbage=42;
),编译器无法消除该语句,无论它与程序的可观察行为有多么无关。在输入catch
块之前,分配需要完成。
对于它的价值,finally
讲述了类似的降级故事:
12.3.3.14尝试终结陈述
对于表单的 try 语句 stmt :
try try-block finally finally-block
• try-block 开头的 v 的明确赋值状态与 v 的明确赋值状态相同 stmt 的开头。
• finally-block 开头的 v 的明确赋值状态与 v 的明确赋值状态相同< EM>语句。
• stmt 的终点处 v 的明确分配状态是明确分配的,如果(且仅当): o v 肯定是在 try-block 的终点分配的 o v 明确地分配在 finally-block 的终点 如果在 try-block 内开始控制流转移(例如 goto 语句),并且在 try-block 之外结束,如果在 finally-block 的终点明确分配了 v ,那么 v 也被认为是在该控制流转移上明确分配的。 (这不仅仅是if-if v 在此控制流转移中由于其他原因而被明确分配,那么它仍然被认为是明确分配的。)
12.3.3.15 Try-catch-finally语句
对尝试 - catch - 最后形式语句的明确赋值分析:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n finally finally-block
完成,就好像该语句是 try - finally 语句,包含 try - catch 语句:
try { try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n } finally finally-block
答案 9 :(得分:1)
我真的很喜欢Hafthor的blog post,并且在我的讨论中加上我的两分钱,我想说,我总是很容易让DATA LAYER抛出一种类型的异常(DataAccessException) )。通过这种方式,我的业务层可以了解预期的异常并抓住它。然后,根据进一步的业务规则(即,如果我的业务对象参与工作流等),我可能抛出一个新的异常(BusinessObjectException)或继续而不重新/抛出。
我会说,只要有必要,请毫不犹豫地使用try..catch并明智地使用它!
例如,此方法参与工作流程......
评论
public bool DeleteGallery(int id)
{
try
{
using (var transaction = new DbTransactionManager())
{
try
{
transaction.BeginTransaction();
_galleryRepository.DeleteGallery(id, transaction);
_galleryRepository.DeletePictures(id, transaction);
FileManager.DeleteAll(id);
transaction.Commit();
}
catch (DataAccessException ex)
{
Logger.Log(ex);
transaction.Rollback();
throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
}
}
}
catch (DbTransactionException ex)
{
Logger.Log(ex);
throw new BusinessObjectException("Cannot delete gallery.", ex);
}
return true;
}
答案 10 :(得分:1)
我们可以在迈克尔·斯科特的“编程语言语用学”中读到,现在的编译器在常见情况下不会增加任何开销,这意味着,当没有异常发生时。所以每一项工作都是在编译时完成的。 但是当在运行时抛出异常时,编译器需要执行二进制搜索以找到正确的异常,这将发生在您所做的每个新抛出中。
但例外情况是例外情况,这笔费用完全可以接受。如果您尝试在没有异常的情况下进行异常处理并使用返回错误代码,那么您可能需要为每个子例程使用if语句,这将导致真正的实时开销。您知道if语句被转换为一些汇编指令,每次进入子例程时都会执行这些指令。
对我的英语很抱歉,希望对你有帮助。此信息基于引用的书籍,有关更多信息,请参阅第8.5章“异常处理”。
答案 11 :(得分:-4)
让我们分析try / catch块的最大可能成本之一,在不需要使用的地方使用它:
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
这是没有try / catch的那个:
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
不计算无关紧要的空白区域,可能会注意到这两条等分代码几乎完全相同,以字节为单位。后者包含4个字节的缩进。这是件坏事吗?
为了增加对伤害的侮辱,学生决定循环,而输入可以解析为int。没有try / catch的解决方案可能类似于:
while (int.TryParse(...))
{
...
}
但是使用try / catch时看起来如何?
try {
for (;;)
{
x = int.Parse(...);
...
}
}
catch
{
...
}
尝试/捕获块是浪费缩进的神奇方法,我们甚至不知道它失败的原因!想象一下,当代码继续执行一个严重的逻辑缺陷时,调试人员会感觉如何,而不是停留一个明显的异常错误。 Try / catch块是一个懒人的数据验证/卫生。
其中一个较小的成本是try / catch块确实禁用了某些优化:http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx。我想这也是一个积极的观点。它可用于禁用优化,否则这些优化可能会破坏多线程应用程序的安全,合理的消息传递算法,并捕获可能的竞争条件;)这是我能想到使用try / catch的唯一场景。即便如此也有其他选择。