关于.net:C#中try / catch的实际开销是多少?

关于.net:C#中try / catch的实际开销是多少?

What is the real overhead of try/catch in C#?

所以,我知道try / catch会增加一些开销,因此不是控制流程流的好方法,但是这种开销来自何处以及它的实际影响是什么?


要点三点:

  • 首先,在代码中实际使用try-catch块几乎没有或没有性能损失。在尝试避免将它们放入您的应用程序时,这不应该是一个考虑因素。只有在抛出异常时,性能才会发挥作用。

  • 当除了其他人提到的堆栈展开操作等之外还抛出异常时,你应该知道发生了一大堆运行时/反射相关的东西,以便填充异常类的成员,例如堆栈跟踪对象和各种类型的成员等

  • 我认为,这是为什么一般建议,如果你要重新抛出异常只是throw;而不是再次抛出异常或构造一个新异常的原因之一,因为在这些情况下所有的堆栈信息都被重新调整而在简单的投掷中,它都被保留了下来。


我不是语言实现方面的专家(所以请稍等一下),但我认为最大的成本之一是展开堆栈并将其存储为堆栈跟踪。我怀疑只有当抛出异常时才会发生这种情况(但我不知道),如果是这样的话,每次抛出异常时这都会有相当大的隐藏成本......所以它不像你只是从一个地方跳过在另一个代码中,有很多事情要发生。

只要您使用EXCEPTIONAL行为的异常(因此不是典型的,预期的程序路径),我认为这不是问题。


您是否询问在未抛出异常时使用try / catch / finally的开销,或者使用异常来控制流程的开销?后者有点类似于使用一根炸药点燃幼儿的生日蜡烛,相关的开销分为以下几个方面:

  • 由于抛出的异常访问缓存中通常不存在的常驻数据,因此可能会出现额外的缓存未命中。
  • 由于抛出的异常访问非常驻代码和数据通常不在应用程序的工作集中,因此可能会出现其他页面错误。

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

      • CLR试图将不经常使用的代码和数据放在远离频繁使用的数据以改善局部性的地方,所以这对你不利,因为你迫使感冒变热。
      • 硬页面错误的成本(如果有的话)会使其他所有内容相形见绌。
  • 典型的捕获情况通常很深,因此上述影响往往会被放大(增加页面错误的可能性)。

至于成本的实际影响,这可能会有很大差异,具体取决于当时代码中的其他内容。 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禁用的优化(我将在引号中进一步探讨这一点)从标准)
  • 性能分析ParseTryParseConvertTo由Ian Huff明确表示"异常处理非常缓慢"并通过点击Int.ParseInt.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.

    什么优化trycatchfinally禁用?

    A.K.A

    trycatchfinally如何用作调试辅助工具?

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

    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的明确赋值状态是明确赋值的。 )。
  • 换句话说,在每个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的唯一场景。即使这有其他选择。


    推荐阅读