所以,我知道try / catch会增加一些开销,因此不是控制流程流的好方法,但是这种开销来自何处以及它的实际影响是什么?
要点三点:
-
首先,在代码中实际使用try-catch块几乎没有或没有性能损失。在尝试避免将它们放入您的应用程序时,这不应该是一个考虑因素。只有在抛出异常时,性能才会发挥作用。
-
当除了其他人提到的堆栈展开操作等之外还抛出异常时,你应该知道发生了一大堆运行时/反射相关的东西,以便填充异常类的成员,例如堆栈跟踪对象和各种类型的成员等
-
我认为,这是为什么一般建议,如果你要重新抛出异常只是throw;而不是再次抛出异常或构造一个新异常的原因之一,因为在这些情况下所有的堆栈信息都被重新调整而在简单的投掷中,它都被保留了下来。
我不是语言实现方面的专家(所以请稍等一下),但我认为最大的成本之一是展开堆栈并将其存储为堆栈跟踪。我怀疑只有当抛出异常时才会发生这种情况(但我不知道),如果是这样的话,每次抛出异常时这都会有相当大的隐藏成本......所以它不像你只是从一个地方跳过在另一个代码中,有很多事情要发生。
只要您使用EXCEPTIONAL行为的异常(因此不是典型的,预期的程序路径),我认为这不是问题。
您是否询问在未抛出异常时使用try / catch / finally的开销,或者使用异常来控制流程的开销?后者有点类似于使用一根炸药点燃幼儿的生日蜡烛,相关的开销分为以下几个方面:
至于成本的实际影响,这可能会有很大差异,具体取决于当时代码中的其他内容。 Jon Skeet在这里有一个很好的总结,有一些有用的链接。我倾向于同意他的观点,即如果你达到了异常会严重影响你的表现的程度,那么你在使用异常方面就会遇到问题而不仅仅是表现。
根据我的经验,最大的开销是实际抛出异常并处理它。我曾经在一个项目中工作,其中使用类似于以下的代码来检查某人是否有权编辑某个对象。这个HasRight()方法在表示层中的任何地方都使用,并且通常被称为100个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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个数量级:
1 2 3 4 5 6 7 8
| bool HasRight(string rightName, DomainObject obj) {
return _user.Rights.Contains(rightName);
}
void CheckRight(string rightName, DomainObject obj) {
if (!HasRight(rightName, obj))
throw new Exception();
} |
因此,简而言之,在正常流程中使用异常比使用类似的流程流程慢两个数量级,没有例外。
与通常接受的理论相反,try / catch可能会产生重大的性能影响,那就是抛出异常是不是!
它会禁用一些自动优化(按设计),并在某些情况下注入调试代码,正如您可以从调试辅助中获得的那样。在这一点上总会有人不同意我的观点,但是语言需要它并且反汇编显示它所以那些人是通过字典定义妄想。
它会对维护产生负面影响。这实际上是这里最重要的问题,但由于我的上一个答案(几乎完全集中于它)被删除了,我将尝试关注不太重要的问题(微优化)而不是更重要的问题(宏观优化)。
多年来,微软MVP已经在几篇博客文章中介绍了前者,我相信你可以轻松找到它们,但StackOverflow非常关注内容,所以我会提供一些链接作为 filler 罢工>证据:
Peter Ritchie对try / catch / finally(和第二部分)的性能影响探讨了try / catch / finally禁用的优化(我将在引号中进一步探讨这一点)从标准)
性能分析Parse与TryParse对ConvertTo由Ian Huff明确表示"异常处理非常缓慢"并通过点击Int.Parse和Int.TryParse来证明这一点......对于任何人坚持TryParse在幕后使用try / catch,这应该有所帮助!
还有这个答案显示了使用try / catch的反汇编代码之间的区别。
看起来很明显,在代码生成中存在明显可观察到的开销,并且这种开销甚至似乎被微软有价值的人所承认!然而,我正在重复互联网......
是的,对于一个简单的代码行,有许多额外的MSIL指令,甚至不包括禁用的优化,因此从技术上讲它是一个微优化。
我在几年前发布了一个答案,因为它专注于程序员的生产力(宏观优化)而被删除了。
这是不幸的,因为在这里和那里没有节省几纳秒的CPU时间可能弥补人类手动优化的许多累积小时数。你的老板支付更多费用:一小时的时间,或一小时的电脑运行?我们什么时候拔掉插头并承认是时候买一台更快的电脑了?
显然,我们应该优化我们的优先级,而不仅仅是我们的代码!在我的上一个回答中,我借鉴了两段代码之间的差异。
使用try / catch:
1 2 3 4 5 6 7 8
| int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here... |
不使用try / catch:
1 2 3 4 5
| 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有它的好处,它被设计为禁用优化!如果你愿意,它是一个调试辅助工具!这就是它的设计目标,它应该被用作......
I guess that's a positive point too. It can be used to disable optimizations that might otherwise cripple safe, sane message passing algorithms for multithreaded applications, and to catch possible race conditions ;) That's about the only scenario I can think of to use try/catch. Even that has alternatives.
Ok.
什么优化try,catch和finally禁用?
A.K.A
try,catch和finally如何用作调试辅助工具?
他们是写作障碍。这来自标准:
12.3.3.13 Try-catch语句
对于表单的语句stmt:
1 2 3 4
| try try-block
catch ( ... ) catch-block-1
...
catch ( ... ) catch-block-n |
try-block开头的v的明确赋值状态与stmt开头的v的明确赋值状态相同。
catch-block-i(对于任何i)开头的v的明确赋值状态与stmt开头的v的明确赋值状态相同。
如果(并且仅当)v在try-block的终点和每个catch-block-i(对于从1到n的每个i)中明确赋值,那么在stmt的结束点处的v的明确赋值状态是明确赋值的。 )。
BLOCKQUOTE>
换句话说,在每个try语句的开头:
在输入try语句之前对可见对象进行的所有赋值都必须完成,这需要一个线程锁定才能启动,这对调试竞争条件很有用!
不允许编译器:
消除在try语句之前已明确分配的未使用的变量赋值
重组或合并任何内部任务(即,如果你还没有这样做,请参见我的第一个链接)。
在这个障碍上提升任务,延迟分配给它知道的变量,直到以后(如果有的话)不会使用它或者先发制人地移动以后的任务以使其他优化成为可能......
每个catch声明都有类似的故事;假设在你的try语句(或它调用的构造函数或函数等)中你分配给那个无意义的变量(比方说,garbage=42;),编译器无法消除该语句,无论它与多么无关。程序的可观察行为。在输入catch块之前,需要完成分配。
对于它的价值,finally讲述了一个同样有辱人格的故事:
12.3.3.14 Try-finally语句
For a try statement stmt of the form:
Ok.
1 2
| try try-block
finally finally-block |
? The definite assignment state of v at the beginning of try-block is the same as the definite assignment state of v at the beginning of stmt.
? The definite assignment state of v at the beginning of finally-block is the same as the definite assignment state of v at the beginning of stmt.
? The definite assignment state of v at the end-point of stmt is definitely assigned if (and only if) either:
o v is definitely assigned at the end-point of try-block
o v is definitely assigned at the end-point of finally-block
If a control flow transfer (such as a goto statement) is made that begins within try-block, and ends outside of try-block, then v is also considered definitely assigned on that control flow transfer if v is definitely assigned at the end-point of finally-block. (This is not an only if—if v is definitely assigned for another reason on this control flow transfer, then it is still considered definitely assigned.)
Ok.
12.3.3.15 Try-catch-finally语句
Definite assignment analysis for a try-catch-finally statement of the form:
Ok.
1 2 3 4 5
| try try-block
catch ( ... ) catch-block-1
...
catch ( ... ) catch-block-n
finally finally-block |
is done as if the statement were a try-finally statement enclosing a try-catch statement:
Ok.
1 2 3 4 5 6 7 8
| try {
try
try-block
catch ( ... ) catch-block-1
...
catch ( ... ) catch-block-n
}
finally finally-block |
好。
我曾经写过一篇关于这篇文章的文章,因为当时有很多人都在询问这个问题。您可以在http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx找到它和测试代码。
结果是try / catch块有很小的开销但是很小,应该被忽略。但是,如果在执行数百次的循环中运行try / catch块,则可能需要考虑将块移动到循环外部(如果可能)。
try / catch块的关键性能问题是当您实际捕获异常时。这可能会给您的应用程序带来明显的延迟。当然,当出现问题时,大多数开发人员(以及许多用户)都认为暂停是一个即将发生的例外!这里的关键是不要对正常操作使用异常处理。顾名思义,它们非常特殊,您应该尽一切可能避免它们被抛出。您不应将它们用作正常运行的程序的预期流程的一部分。
更不用说它是否在一个经常被调用的方法中,它可能会影响应用程序的整体行为。
例如,我认为在大多数情况下使用Int32.Parse是一种不好的做法,因为它会抛出一些容易被捕获的异常。
所以写下这里写的所有内容:
1)使用try..catch块来捕获意外错误 - 几乎没有性能损失。
2)如果可以避免,则不要对例外错误使用例外。
编写,调试和维护没有编译器错误消息,代码分析警告消息和例程接受的异常(特别是在一个地方抛出并在另一个地方被接受的异常)的代码要容易得多。因为它更容易,所以代码平均可以更好地编写并减少错误。
对我来说,程序员和质量开销是反对使用try-catch进行流程的主要论据。
相比之下,异常的计算机开销是微不足道的,并且在应用程序满足实际性能要求的能力方面通常很小。
我去年做了关于这个主题的博客文章。
看看这个。最重要的是,如果没有异常发生,那么try块几乎没有任何成本 - 而在我的笔记本电脑上,一个例外是大约36μs。这可能比你预期的要少,但要记住那些结果在浅堆栈上。此外,第一个例外真的很慢。
我们可以在Michael L. Scott的Programming Languages Pragmatics中读到,现在的编译器在常见情况下不会增加任何开销,这意味着,当没有异常发生时。所以每一项工作都是在编译时完成的。
但是当在运行时抛出异常时,编译器需要执行二进制搜索以找到正确的异常,这将发生在您所做的每个新抛出中。
但例外情况是例外,这种成本是完全可以接受的。如果您尝试在没有异常的情况下进行异常处理并使用返回错误代码,那么您可能需要为每个子例程使用if语句,这将导致真正的实时开销。您知道if语句转换为一些汇编指令,每次进入子例程时都会执行这些指令。
抱歉我的英文,希望对你有帮助。此信息基于引用的书籍,有关更多信息,请参阅第8.5章"异常处理"。
我真的很喜欢Hafthor的博客文章,为了加上我的两分钱,我想说,我总是很容易让DATA LAYER抛出一种异常(DataAccessException)。通过这种方式,我的业务层可以了解预期的异常并抓住它。然后,根据进一步的业务规则(即,如果我的业务对象参与工作流等),我可能抛出一个新的异常(BusinessObjectException)或继续而不重新/抛出。
我会说,只要有必要,请毫不犹豫地使用try..catch并明智地使用它!
例如,此方法参与工作流程...
评论?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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;
} |
让我们分析try / catch块在使用时不需要使用的最大可能成本之一:
1 2 3 4 5 6 7 8
| int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here... |
这是没有try / catch的那个:
1 2 3 4 5
| int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here |
不计算无关紧要的空白区域,人们可能会注意到这两条等分代码几乎完全相同的字节长度。后者包含4个字节的缩进。那是一件坏事?
为了增加对伤害的侮辱,学生决定循环,同时可以将输入解析为int。没有try / catch的解决方案可能是这样的:
1 2 3 4
| while (int.TryParse(...))
{
...
} |
但是当使用try / catch时这看起来如何?
1 2 3 4 5 6 7 8 9 10 11
| try {
for (;;)
{
x = int.Parse(...);
...
}
}
catch
{
...
} |
Try / catch块是浪费缩进的神奇方式,我们甚至不知道它失败的原因!想象一下,当代码继续执行一个严重的逻辑缺陷时,调试人员会感觉如何,而不是停留一个明显的异常错误。 Try / catch块是懒人的数据验证/卫生。
其中一个较小的成本是try / catch块确实禁用了某些优化:http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx 。我想这也是一个积极的观点。它可用于禁用优化,否则这些优化可能会破坏多线程应用程序的安全,合理的消息传递算法,并捕获可能的竞争条件;)这是我能想到使用try / catch的唯一场景。即使这有其他选择。