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