在C#中try / catch的实际开销是多少?


Answers:


52

我不是语言实现方面的专家(因此请耐心等待),但是我认为最大的成本之一就是展开堆栈并将其存储以进行堆栈跟踪。我怀疑只有在抛出异常时才会发生这种情况(但我不知道),如果是这样,那么每次抛出异常时,这将是相当大的隐藏成本...因此,这不像您只是从一个地方跳下来在另一个代码中,发生了很多事情。

我不认为这是一个问题,只要您对异常行为使用异常(这样就不会在程序中使用通常的预期路径)。


33
更准确地说:尝试便宜,抓捕便宜,投掷昂贵。如果您避免尝试接球,则掷球仍然很昂贵。
Windows程序员

4
嗯-标记不适用于评论。要再次尝试-例外是错误的,而不是为“异常行为”或条件:blogs.msdn.com/kcwalina/archive/2008/07/17/...
HTTP 410

2
@Windows程序员统计信息/来源?
卡佩2014年

100

这里要说明三点:

  • 首先,在代码中实际包含try-catch块几乎没有性能损失。尝试避免将它们包含在您的应用程序中时,这不是考虑因素。仅在引发异常时才会影响性能。

  • 当除了其他人提到的堆栈展开操作等引发异常时,您应该意识到,发生了一系列与运行时/反射相关的事情,以便填充异常类的成员,例如堆栈跟踪对象和各种类型的成员等。

  • 我认为,这就是为什么要抛出异常的一般建议是只是throw;而不是再次抛出异常或构造一个新的异常,原因是在这些情况下,所有堆栈信息都被收集,而在简单情况下,扔掉它全部保留下来。


41
另外:当您将异常重新抛出为“ throw ex”时,您将丢失原始堆栈跟踪,并将其替换为CURRENT堆栈跟踪;很少想要什么。如果只是“抛出”,则将保留Exception中的原始堆栈跟踪。
Eddie

@Eddie Orthrow new Exception("Wrapping layer’s error", ex);
binki

21

您是否在询问没有抛出异常时使用try / catch / finally的开销,还是使用异常来控制流程的开销?后者有点类似于用炸药点燃幼儿的生日蜡烛,相关的开销落在以下区域:

  • 由于抛出的异常访问通常不在高速缓存中的常驻数据,您可能会预期到其他高速缓存未命中。
  • 由于引发的异常访问应用程序工作集中通常不访问的非驻留代码和数据,您可能会遇到其他页面错误。

    • 例如,抛出异常将要求CLR根据当前IP和每帧的返回IP找到finally和catch块的位置,直到处理了异常加上过滤块为止。
    • 为了创建用于诊断目的的框架(包括读取元数据等),需要额外的建造成本和名称解析。
    • 以上两个项目通常都访问“冷”代码和数据,因此如果您完全有内存压力,则很可能发生硬页面错误:

      • CLR会尝试将不经常使用的代码和数据与经常使用的数据区分开来,以改善局部性,因此这对您不利,因为您正在强迫感冒变热。
      • 硬页面错误的成本(如果有的话)会使所有其他因素相形见.。
  • 典型的捕获情况通常很深,因此上述影响往往会被放大(增加页面错误的可能性)。

至于成本的实际影响,这可能会变化很大,具体取决于当时代码中发生的其他事情。乔恩·斯基特(Jon Skeet)在这里有一个不错的摘要,并提供了一些有用的链接。我倾向于同意他的说法,即如果您到达了一个异常会严重影响您的性能的地步,那么您在使用异常方面就存在着性能之外的问题。


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();
}

当测试数据库充满测试数据时,这会导致打开新表格等时的速度明显下降。

因此,我将其重构为以下内容-根据稍后的快速'n脏测量-大约快了两个数量级:

bool HasRight(string rightName, DomainObject obj) {
  return _user.Rights.Contains(rightName);
}

void CheckRight(string rightName, DomainObject obj) {
  if (!HasRight(rightName, obj))
    throw new Exception();
}

简而言之,在正常流程中使用例外要比在没有例外的情况下使用类似流程慢两个数量级。


1
您为什么要在这里抛出异常?您可以当场解决没有权利的情况。
ThunderGr13年

@ThunderGr实际上就是我所做的更改,使其速度提高了两个数量级。
Tobi

5

与普遍接受的理论相反,try/ catch可能会对性能产生重大影响,那就是是否引发异常!

  1. 它会禁用某些自动优化(通过设计),并且在某些情况下会注入调试代码,正如您从调试工具中所期望的那样。在这一点上总会有人不同意我,但是语言要求它并且反汇编显示出来,因此根据字典定义,这些人是妄想的
  2. 它会对维护产生负面影响。这实际上是这里最重要的问题,但是由于删除了我的最后一个答案(几乎完全针对此问题),我将尝试着眼于次要问题(微观优化),而不是次要问题(微观优化)宏观优化)。

前者已被覆盖由微软MVP的一对夫妇的博客文章多年来,我相信你可以很容易地找到他们又StackOverflow的关心这么多有关内容,所以我会提供链接到一些他们作为填充物的证据:

还有一个答案,显示了使用-和不使用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隐藏在一些自制的抽象后面,但是...那么我们最好也避免出现异常(即use Int.TryParse)。

这甚至不是一个复杂的例子。我已经看到了在try/ 中实例化新类的尝试catch。考虑到构造函数内部的所有代码随后可能会失去某些优化的资格,否则这些优化将由编译器自动应用。有什么更好的方法可以使编译器运行缓慢,而不是完全按照编译器的指示运行呢?

假设所述构造函数引发异常,并因此触发了一些错误,则维护欠佳的开发人员必须对其进行跟踪。与goto噩梦的意大利面条代码不同,try/ catch可能会导致三个方面的混乱,因为它不仅可以移到同一方法的其他部分,而且还可以移入同一方法的其他部分,因此可能并非易事,所有这些都会由维护开发人员严格遵守!但是我们被告知“ goto很危险”,嘿!

最后,我提到了try/ catch的好处是,它旨在禁止优化!如果愿意的话,它是调试的辅助工具!这就是它的设计宗旨,应将其用作...

我想这也是一个积极的观点。它可以用来禁用可能会破坏多线程应用程序的安全,理智的消息传递算法的优化,并捕获可能的争用条件;)那是我想到的唯一尝试使用try / catch的方案。即使那样,也有其他选择。


什么做的优化trycatchfinally禁用?

又名

怎么样trycatchfinally作为调试工具有用吗?

它们是写障碍。这来自标准:

12.3.3.13 Try-catch语句

对于以下形式的语句stmt

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
  • 的明确赋值状态v之初试块相同的明确赋值状态v之初语句
  • catch-block-i开头的v的明确赋值状态(对于任何i)与在stmt开头的v的明确赋值状态相同。
  • v(在且仅当)v明确分配在try块和每个catch-block-i端点上(对于从1到n的每个i),才确定在stmt端点处v的确定分配状态)。

换句话说,在每个try语句的开头:

  • 在输入try语句之前,对可见对象所做的所有分配必须完成,这需要一个线程锁作为开始,这对于调试竞争条件很有用!
  • 不允许编译器执行以下操作:
    • 消除在try语句之前已明确分配给未使用的变量分配
    • 重新组织或合并任何内部分配(例如,如果您尚未这样做,请参阅我的第一个链接)。
    • 提升此障碍的分配,以延迟分配给它知道不会在以后使用(如果有的话)的变量,或者抢先将以后的分配向前移动,以使其他优化成为可能。

每个catch陈述都有相似的故事。假设在您的try语句(或其调用的构造函数或函数等)中分配了该无意义的变量(例如garbage=42;),则编译器无法消除该语句,无论该语句与程序的可观察行为有多大关系。进入块之前,需要完成分配catch

对于它的价值,finally讲述一个同样可耻的故事:

12.3.3.14 Try-finally语句

对于以下形式的try语句stmt

try try-block
finally finally-block

try-block 开头的v的明确分配状态与stmt开头的v的明确分配状态相同。 • finally块开头的v的明确分配状态与stmt开头的v的明确分配状态相同。 •的明确赋值状态v在终点语句是明确赋值如果(且仅当)两种:○ v在结束点明确分配的试块 Ø v

在终点是明确赋值最后块 如果控制流转移(如转到语句)由内开始试块,并且外端部的try块,然后v也视为已明确对分配如果将v明确地指定在finally块的端点,则控制流传输。(这不是唯一的,如果-如果在此控制流传输中由于其他原因而明确指定了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   
    try-block
    catch ( ... ) catch-block-1
    ...   
    catch ( ... ) catch-block-n
} 
finally finally-block

3

更不用说它是否在常用方法内,它可能会影响应用程序的整体行为。
例如,在大多数情况下,我认为Int32.Parse的使用是一种不好的做法,因为它会引发某些异常,而这些异常本来可以很容易地捕获的。

因此,总结一下这里写的所有内容:
1)使用try..catch块捕获意外错误-几乎不降低性能。
2)如果可以避免,请不要对例外错误使用例外。


3

不久前,我写了一篇有关此的文章,因为当时有很多人在问这个。您可以在http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx上找到它和测试代码。

结果是,try / catch块的开销很小,但是很小,应该忽略。但是,如果要在执行了数百万次的循环中运行try / catch块,则可能需要考虑将块移到循环外部。

try / catch块的关键性能问题是实际捕获异常时。这会给您的应用程序增加明显的延迟。当然,当出现问题时,大多数开发人员(以及很多用户)都将暂停视为即将发生的异常!这里的关键是不要在正常操作中使用异常处理。顾名思义,它们是特殊的,您应该尽一切可能避免它们被抛出。您不应将它们用作正常运行的程序的预期流程的一部分。


2

我去年在此主题上写了一篇博客文章。看看这个。最重要的是,如果没有异常发生,try块几乎没有成本-在我的笔记本电脑上,异常约为36μs。这可能比您预期的要少,但是请记住,这些结果在浅层堆栈中。另外,第一个例外真的很慢。


我无法访问您的博客(连接正在超时;您使用的是try/ catch太多了吗?与您的建议相反...我很乐意提出我所做的研究是错误的建议,但我需要阅读您的博客条目以了解其内容。
自闭症

除了@Hafthor的博客文章之外,这是另一篇博客文章,其代码专门用于测试速度性能差异。根据结果​​,如果您仅在5%的时间内发生异常,则异常处理代码的整体运行速度将比非异常处理代码慢100倍。本文专门针对try-catchblock vs tryparse()方法,但是概念是相同的。

2

编写,调试和维护没有编译器错误消息,代码分析警告消息和例行接受的异常(尤其是放在一个地方并在另一个地方接受的异常)的代码要容易得多。因为它更容易,所以平均而言,该代码将更好地编写,而错误则更少。

对我来说,程序员和质量开销是反对使用try-catch进行流程的主要理由。

相比之下,异常的计算机开销微不足道,并且就应用程序满足实际性能要求的能力而言,通常很少。


@Ritchard T,为什么?与程序员和质量开销相比它是微不足道的。
ThunderGr

1

我真的很喜欢Hafthor的博客文章,在此讨论中加我两分钱,我想说的是,让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;
}

2
大卫,您会在try / catch块中包装对“ DeleteGallery”的调用吗?
罗布

由于DeleteGallery是布尔函数,在我看来在其中抛出异常没有用。这将要求对DeleteGallery的调用包含在try / catch块中。if(!DeleteGallery(theid)){// handle}对我来说更有意义。在那个具体的例子中。
ThunderGr 2013年

1

我们可以从Michael L. Scott的《 Programming Languages Pragmatics》一书中读到,当今的编译器在一般情况下不会增加任何开销,这意味着没有异常发生。因此,每项工作都是在编译时完成的。但是,当在运行时引发异常时,编译器需要执行二进制搜索以查找正确的异常,并且对于您进行的每个新抛出都会发生这种情况。

但是例外就是例外,这笔费用是完全可以接受的。如果您尝试在没有异常的情况下进行异常处理,而改用返回错误代码,则可能需要为每个子例程添加一个if语句,这将导致真正的实时开销。您知道if语句将转换为一些汇编指令,该指令将在您每次输入子例程时执行。

对不起,我的英语,希望对您有帮助。该信息基于引用的书,有关更多信息,请参见第8.5章“异常处理”。


编译器在运行时不可用。目前成为try / catch块的开销,以便CLR可以处理的异常。C#在.NET CLR(虚拟机)上运行。在我看来,当没有异常时,块本身的开销是最小的,但是CLR处理异常的成本非常高。
ThunderGr

-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块确实确实禁用了某些优化:http : //msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx。我想这也是一个积极的观点。它可以用来禁用可能会破坏多线程应用程序的安全,理智的消息传递算法的优化,并捕获可能的争用条件;)那是我想到的唯一尝试使用try / catch的方案。即使那样,也有其他选择。


1
我很确定TryParse会尝试{int x = int.Parse(“ xxx”); 返回true;} catch {返回false; }内部。缩进不是问题所在,只是性能和开销。
ThunderGr

@ThunderGr或者,阅读我发布的新答案。它包含了更多的链接,当你避免其中之一是大量性能提升的分析Int.Parse支持Int.TryParse
自闭症
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.