所以,我知道 try/catch 确实增加了一些开销,因此不是控制流程的好方法,但是这个开销来自哪里,它的实际影响是什么?
12 回答
这里要说明三点:
首先,在代码中实际使用 try-catch 块几乎没有或没有性能损失。当试图避免将它们包含在您的应用程序中时,这不应该是一个考虑因素。只有在抛出异常时才会对性能造成影响。
当除了其他人提到的堆栈展开操作等之外引发异常时,您应该知道会发生一大堆与运行时/反射相关的事情,以便填充异常类的成员,例如堆栈跟踪对象和各种类型的成员等。
我相信这就是为什么如果你要重新抛出异常的一般建议是只是
throw;
而不是再次抛出异常或构造一个新异常,因为在这些情况下,所有堆栈信息都被重新收集,而在简单的扔掉它全部保存下来。
我不是语言实现方面的专家(所以对此持保留态度),但我认为最大的成本之一是展开堆栈并将其存储为堆栈跟踪。我怀疑这只发生在抛出异常时(但我不知道),如果是这样,每次抛出异常时这将是相当大的隐藏成本......所以这不像你只是从一个地方跳下来在另一个代码中,发生了很多事情。
只要您对异常行为使用异常(因此不是您通过程序的典型预期路径),我认为这不是问题。
您是在询问未引发异常时使用 try/catch/finally 的开销,还是使用异常控制流程流的开销?后者有点类似于使用炸药棒点燃幼儿的生日蜡烛,相关的开销分为以下几个方面:
- 由于抛出的异常访问通常不在缓存中的常驻数据,您可能会遇到额外的缓存未命中。
由于抛出的异常访问应用程序工作集中不正常的非常驻代码和数据,您可能会遇到额外的页面错误。
- 例如,抛出异常将需要 CLR 根据当前 IP 和每帧的返回 IP 找到 finally 和 catch 块的位置,直到处理异常加上过滤块。
- 额外的构建成本和名称解析,以便创建用于诊断目的的帧,包括读取元数据等。
上述两项通常都访问“冷”代码和数据,因此如果您有内存压力,则可能会出现硬页面错误:
- CLR 试图将不经常使用的代码和数据远离经常用于改善局部性的数据,因此这对您不利,因为您正在迫使冷变热。
- 硬页错误的代价(如果有的话)将使其他一切相形见绌。
- 典型的捕获情况通常很深,因此上述影响往往会被放大(增加页面错误的可能性)。
至于成本的实际影响,这可能会因当时代码中发生的其他情况而有很大差异。Jon Skeet 在这里有一个很好的总结,有一些有用的链接。我倾向于同意他的说法,即如果您遇到异常严重损害您的性能的地步,那么您在使用异常方面会遇到问题,而不仅仅是性能。
以我的经验,最大的开销是实际抛出异常并处理它。我曾经参与过一个项目,其中使用类似于以下的代码来检查是否有人有权编辑某些对象。这个 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();
}
当测试数据库充满测试数据时,这会导致在打开新表单等时出现非常明显的减速。
所以我将它重构为以下内容,根据后来的快速“n脏测量”,它快了大约 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();
}
所以简而言之,在正常流程中使用异常比在没有异常的情况下使用类似流程要慢两个数量级。
与普遍接受的理论相反,try
/catch
可能会对性能产生重大影响,这就是是否引发异常!
- 它禁用了一些自动优化(按设计),并且在某些情况下会注入调试代码,正如您可以从调试辅助工具中所期望的那样。在这一点上总会有人不同意我的观点,但是语言需要它并且反汇编显示它,所以这些人在字典定义中是妄想的。
- 它会对维护产生负面影响。这实际上是这里最重要的问题,但是由于我的最后一个答案(几乎完全集中在它上)被删除了,我将尝试关注不太重要的问题(微优化)而不是更重要的问题(宏观优化)。
多年来,Microsoft MVP 已在几篇博客文章中介绍了前者,我相信您可以轻松找到它们,但 StackOverflow 非常关心内容,因此我将提供其中一些链接作为补充证据:
try
/catch
/finally
(和第二部分)的性能影响,作者 Peter Ritchie 探讨了try
/catch
/finally
禁用的优化(我将通过引用标准进一步探讨)- Ian Huff 的Performance Profiling
Parse
vs.TryParse
vs.ConvertTo
公然指出“异常处理非常缓慢”,并通过相互对抗来证明这一点Int.Parse
......Int.TryParse
对于任何坚持在幕后TryParse
使用try
/catch
还有这个答案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
禁用?
又名
作为调试辅助工具有什么try
用处?catch
finally
它们是写屏障。这来自标准:
12.3.3.13 Try-catch 语句
对于以下形式的语句stmt:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n
- v在try-block开头的确定赋值状态与v在stmt开头的确定赋值状态相同。
- 在catch-block-i开头的v的明确赋值状态(对于任何i )与在stmt开头的v的明确赋值状态相同。
- 当(且仅当)v在 try-block 的端点和每个catch - block -i(对于从 1 到n的每个i)。
换句话说,在每个try
语句的开头:
- 在进入语句之前对可见对象所做的所有分配都
try
必须完成,这需要一个线程锁才能开始,这对于调试竞争条件很有用! - 编译器不允许:
try
消除在语句之前已经明确分配的未使用的变量分配- 重组或合并它的任何内部任务(即,如果您还没有这样做,请参阅我的第一个链接)。
- 在这个障碍上提升分配,延迟分配到它知道以后不会使用的变量(如果有的话),或者先发制人地向前移动以后的分配以使其他优化成为可能......
每个catch
陈述都有一个类似的故事;假设在您的try
语句(或它调用的构造函数或函数等)中,您分配给那个原本毫无意义的变量(比如说,garbage=42;
),编译器无法消除该语句,无论它与程序的可观察行为多么无关. 分配需要在进入块之前完成。catch
对于它的价值,finally
讲述了一个类似的有辱人格的故事:
12.3.3.14 Try-finally 语句
对于以下形式的try语句stmt:
try try-block finally finally-block
• v在try-block 开头的确定赋值状态与v在stmt开头的确定赋值状态相同。
• finally-block开头的v的确定赋值状态与stmt开头的v的确定赋值状态相同。 •在stmt的端点处 v的明确赋值状态当(且仅当)以下任一情况下是绝对赋值的:
绝对分配在finally-block的终点 如果进行的控制流转移(例如goto语句)从 try-block 开始,并在try-block之外结束,那么v也被认为是绝对分配的如果v在finally-block的终点明确分配,则控制流转移。(这不是唯一的,如果v在此控制流传输中出于其他原因被明确分配,那么它仍然被认为是明确分配的。)
12.3.3.15 Try-catch-finally 语句
形式的try - catch - finally语句的确定赋值分析:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n finally finally-block
就像语句是包含try - catch语句的try - finally语句一样完成:
try { try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n } finally finally-block
更不用说它是否在经常调用的方法中,它可能会影响应用程序的整体行为。
例如,我认为在大多数情况下使用 Int32.Parse 是一种不好的做法,因为它会抛出异常,否则很容易被捕获。
所以总结一下这里写的所有内容:
1)使用 try..catch 块来捕获意外错误 - 几乎没有性能损失。
2)如果可以避免,请不要对异常错误使用异常。
前段时间我写了一篇关于这个的文章,因为当时有很多人问这个问题。您可以在http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx找到它和测试代码。
结果是 try/catch 块的开销很小,但太小了,应该被忽略。但是,如果您在循环中运行数百万次的 try/catch 块,则可能需要考虑将块移到循环之外。
try/catch 块的关键性能问题是当您实际捕获异常时。这会给您的应用程序增加明显的延迟。当然,当事情出错时,大多数开发人员(以及许多用户)都将暂停视为即将发生的异常!这里的关键是不要对正常操作使用异常处理。顾名思义,它们是特殊的,您应该尽一切可能避免它们被抛出。您不应将它们用作正常运行的程序的预期流程的一部分。
去年我写了一篇关于这个主题的博客文章。看看这个。底线是,如果没有发生异常,尝试块几乎没有成本 - 在我的笔记本电脑上,异常大约是 36μs。这可能比您预期的要少,但请记住,这些结果在浅堆栈上。此外,第一个例外真的很慢。
编写、调试和维护没有编译器错误消息、代码分析警告消息和例行接受的异常(尤其是在一个地方抛出并在另一个地方接受的异常)的代码要容易得多。因为它更容易,所以代码平均会写得更好,错误更少。
对我来说,程序员和质量开销是反对使用 try-catch 处理流程的主要论据。
相比之下,异常的计算机开销微不足道,就应用程序满足实际性能要求的能力而言,通常微不足道。
我真的很喜欢 Hafthor 的博客文章,并且为了在这个讨论中添加我的两分钱,我想说的是,让数据层只抛出一种类型的异常 (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;
}
我们可以在 Michael L. Scott 的 Programming Languages Pragmatics 中读到,现在的编译器在常见情况下不会增加任何开销,这意味着当没有异常发生时。所以每一项工作都是在编译时完成的。但是当在运行时抛出异常时,编译器需要执行二进制搜索以找到正确的异常,并且这将发生在您所做的每一次新的抛出中。
但是例外就是例外,这个成本是完全可以接受的。如果您尝试在没有异常的情况下进行异常处理并改用返回错误代码,则可能您需要为每个子例程添加一个 if 语句,这将产生真正的实时开销。您知道 if 语句被转换为一些汇编指令,每次您进入子例程时都会执行这些指令。
对不起我的英语,希望它对你有帮助。此信息基于引用的书籍,有关详细信息,请参阅第 8.5 章异常处理。
让我们分析一下在不需要使用它时使用 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 块是懒人的数据验证/清理。
较小的成本之一是 try/catch 块确实禁用了某些优化: http: //msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . 我想这也是一个积极的观点。它可用于禁用优化,否则可能会削弱多线程应用程序的安全、健全的消息传递算法,并捕获可能的竞争条件;)这是我能想到的唯一使用 try/catch 的场景。即便如此,也有其他选择。