【问题标题】:Under C# how much of a performance hit is a try, throw and catch block在 C# 下,try、throw 和 catch 块对性能的影响有多大
【发布时间】:2009-04-15 17:48:37
【问题描述】:

首先,免责声明: 我有其他语言的经验,但仍在学习 C# 的精妙之处

关于问题...我正在查看一些代码,它以与我有关的方式使用 try/catch 块。当调用解析例程时,程序员没有返回错误代码,而是使用了以下逻辑

catch (TclException e) {
  throw new TclRuntimeError("unexpected TclException: " + e.Message,e);
}  

这被调用者捕获,并引发相同的错误...
...被调用者捕获,并引发相同的错误...
.....被调用者捕获,抛出相同的错误......

备份大约 6 个级别。

我是否认为所有这些 catch/throw 块都会导致性能问题,或者这是 C# 下的合理实现?

【问题讨论】:

  • 此处显示的 catch 块确实为异常对象添加了附加信息,这可能证明它的存在是合理的。但是如果 catch 块没用,就把它去掉。

标签: c# performance catch-block


【解决方案1】:

在任何语言下都是糟糕的设计。

异常被设计为在您可以处理的级别被捕获。捕获异常,然后再次抛出它只是浪费时间(它还会导致您丢失有关原始错误位置的宝贵信息)。

显然,编写该代码的人曾经使用错误代码,然后在没有真正了解它们的工作原理的情况下切换到异常。如果在某一级别没有捕获,异常会自动“冒泡”堆栈。

另外,请注意,exceptional 事件是例外。应该永远发生的事情。它们不应该用于正常的有效性检查(即,不要捕获被零除的异常;事先检查除数是否为零)。

【讨论】:

  • 问题中提供的示例中没有丢失任何原始信息,因为原始异常作为 innerException 参数传递给 TclRuntimeError 构造函数。
  • 确实如此。然而,实际的异常被隐藏在用户面临的异常的六层深处。
  • 您通常应该只在抽象点包装异常,即使这样,也只有在您将提供有关错误的其他信息时。例如包装一个 3rd 方组件并包装任何奇怪的异常 (IndexOutOfBounds->KeyNotFound)
  • 异常不是针对永远不会发生的事情,而是针对罕见的错误情况。它们比正常的控制流慢,但每秒有几个异常不会导致性能问题(除非你在调试器中运行。)
【解决方案2】:

根据msdn:Performance Tips and Tricks,您可以使用 try 和 catch 而不会出现任何性能问题直到真正的抛出发生

【讨论】:

  • 你为我节省了很多与该链接的同事辩论 :) 如果您不想费心检查它,这里是文章的直接引述:“查找和设计掉异常密集的代码可以带来不错的性能胜利。请记住,这与 try/catch 块无关:只有在引发实际异常时才会产生成本。您可以使用尽可能多的 try /catch 块随心所欲。 无缘无故地使用异常是您损失性能的地方。例如,您应该远离诸如使用异常进行控制流之类的事情。”
  • @kayleeFrye_onDeck 是的,您可以尽可能多地使用 try 和 catch 没有任何问题。如果在常规程序流程中使用 throw 场景,则唯一的问题。
【解决方案3】:

投掷(而不是接球)是昂贵的。

除非您打算做一些有用的事情(即转换为更有用的异常,处理错误),否则不要放入 catch 块。

只是重新抛出异常(不带参数的 throw 语句),或者更糟糕的是,抛出与刚刚捕获的对象相同的对象绝对是错误的。

编辑:为避免歧义:

重新抛出:

catch (SomeException) {
  throw;
}

从先前的异常对象创建异常,其中所有运行时提供的状态(特别是堆栈跟踪)都被覆盖:

catch (SomeException e) {
  throw e;
}

后一种情况是丢弃有关异常信息的毫无意义的方法。并且在 catch 块中的 throw 之前没有任何东西是毫无意义的。可能会更糟:

catch (SomeException e) {
  throw new SomeException(e.Message);
}

它几乎丢失了 e 包含的所有有用状态信息(包括最初抛出的内容。)

【讨论】:

  • 是否有用包括在重新抛出之前记录错误,还是在其他地方更好地处理(例如应用程序错误)?
  • 重新抛出并不昂贵,因为不必创建 Exception 对象(这涉及展开堆栈)
  • 但是在每一层,他们不是在展开堆栈吗?
  • @devinb:在这种情况下它们是,因为它们抛出了新的异常。如果他们简单地使用“throw;”,他们就不会在每一层展开堆栈。记录时,应该使用它。
  • 不是这样,catch 处理程序必须展开才能获取数据 - 包括读取调用堆栈的元数据。每次捕获都必须这样做,否则您将无法打印出调用堆栈。
【解决方案4】:

一般来说,在 .NET 中抛出异常的成本很高。简单地有一个 try/catch/finally 块不是。所以,是的,从性能的角度来看,现有代码很糟糕,因为当它抛出时,它会抛出 5-6 个臃肿的异常,而不会增加任何价值,而不是简单地让原始异常自然地冒出 5-6 个堆栈帧。

更糟糕的是,从设计的角度来看,现有代码确实很糟糕。异常处理的主要好处之一(与返回错误代码相比)是您不需要到处检查异常/返回代码(在调用堆栈中)。您只需在您真正想要处理它们的少数地方捕获它们。忽略异常(与忽略返回码不同)不会忽略或隐藏问题。这只是意味着它将在调用堆栈的更高层处理。

【讨论】:

  • 显然这一切都需要编写。但作为第一步,是否可以像将 catch (TclException e){throw e;} 更改为 catch (TclException e){throw;} 以允许异常在堆栈中冒泡一样简单?
  • 是的,但比这更容易。只需删除整个 catch(TclException e) 块。如果你没有捕捉到这个异常,它会自己在调用栈中冒泡。
【解决方案5】:

这本身并不可怕,但确实应该注意嵌套。

可接受的用例是这样的:

我是一个低级组件,可能会遇到许多不同的错误,但是我的消费者只对特定类型的异常感兴趣。因此我可以这样做:

catch(IOException ex)
{
    throw new PlatformException("some additional context", ex);
}

现在这允许消费者做:

try
{
    component.TryThing();
}
catch(PlatformException ex)
{
   // handle error
}

是的,我知道有些人会说,但是消费者应该捕获 IOException ,但这取决于消费代码的实际抽象程度。如果 Impl 正在将某些内容保存到磁盘并且消费者没有正当理由认为他们的操作会触及磁盘怎么办?在这种情况下,将此异常处理放在消费代码中是没有意义的。

我们通常试图通过使用这种模式来避免在业务逻辑代码中放置一个“包罗万象”的异常处理程序,因为我们希望找出所有可能的异常类型,因为它们可能会导致更根本的问题,即需要调查。 如果我们没有捕捉到,它会冒泡,命中“顶级”级别的处理程序,应该阻止应用继续运行。这意味着客户报告该异常,您将有机会对其进行调查。当您尝试构建强大的软件时,这一点很重要。您需要找到所有这些错误情况并编写特定的代码来处理它们。

不太漂亮的是嵌套过多,这就是你应该用这段代码解决的问题。

正如另一张海报所说,例外是针对合理的异常行为,但不要太过分。基本上,代码应该表达“正常”操作,异常应该处理您可能遇到的潜在问题。

在性能异常方面很好,如果您使用调试器对嵌入式设备进行测试,但在没有调试器的情况下发布时,它们实际上非常快。

人们在讨论异常的性能时忘记的主要事情是,在错误情况下,一切都会变慢,因为用户遇到了问题。当网络中断并且用户无法保存他们的工作时,我们真的关心速度吗?我非常怀疑以几毫秒的速度将错误报告返回给用户是否会有所作为。

在讨论异常时要记住的主要准则是在正常的应用程序流程中不应发生异常(正常意味着没有错误)。其他一切都源于该声明。

在您给出的确切示例中,我不确定。在我看来,将看似通用的 tcl 异常包装在另一个通用的听起来 tcl 异常中并没有真正获得任何好处。如果有的话,我建议追踪代码的原始创建者并了解他的想法背后是否有任何特定的逻辑。不过,您可能会直接杀死捕获物。

【讨论】:

  • 通常你确实关心性能——在服务器中,如果你只在 1% 的时间里抛出异常,但你每秒接受 100 次调用,超过 100 个用户——那就是每秒 100 次异常!有时这可能会严重影响性能。
【解决方案6】:

Try / Catch / Throw 很慢 - 更好的实现是在捕获之前检查值,但如果您绝对无法继续,最好只在重要时抛出和捕获。否则检查和记录会更有效。

【讨论】:

  • try-block 本身几乎没有开销。这是使系统变慢的例外情况(抛出/捕获)。
  • 正确 - 在他的示例中,重点在于,使用 try/catch 速度较慢,因为它们会导致抛出异常,使用检查然后执行另一个操作而不是连续抛出可能可以避免这种情况。
【解决方案7】:

如果堆栈的每一层都只是用相同的信息重新抛出相同的类型,没有添加任何新内容,那么这完全是荒谬的。

如果它发生在独立开发的库之间的边界,那是可以理解的。有时库作者想要控制从他们的库中逃逸的异常,以便他们以后可以更改其实现,而不必弄清楚如何模拟以前版本的异常抛出行为。

在没有充分理由的情况下,在任何情况下接住并重新投掷通常是个坏主意。这是因为一旦找到 catch 块,throw 和 catch 之间的所有 finally 块都会被执行。仅当可以从中恢复异常时才会发生这种情况。在这种情况下没关系,因为正在捕获特定类型,因此代码作者(希望)知道他们可以安全地撤消自己的任何内部状态更改以响应该特定异常类型。

因此,这些 try/catch 块可能会在设计时产生成本 - 它们使程序更加混乱。但是在运行时,它们只会在抛出异常时产生很大的开销,因为将异常向上传输到堆栈变得更加复杂。

【讨论】:

    【解决方案8】:

    异常很慢,尽量不要使用。

    查看我给here的答案。

    基本上,Chris Brumme(CLR 团队的成员)说它们是作为 SEH 异常实现的,所以当它们被抛出时你会受到很大的打击,当它们在操作系统堆栈中冒泡时,你必须承受惩罚。它是一个真正的excellent article,并深入探讨了抛出异常时发生的情况。例如:


    当然,最大的成本是 例外是当你实际抛出 一。我会在接近尾声时回到这个 的博客。


    性能。异常有直接 实际投掷和接球时的成本 一个例外。他们也可能有一个 与推动相关的间接成本 方法入口的处理程序。和他们 通常会产生隐蔽的成本 限制代码生成机会。


    但是,有一个严重的长期 异常的性能问题 这必须考虑到你的 决定。

    考虑一些事情 抛出异常时发生:

    • 通过解释由 编译器来指导我们的堆栈展开。

    • 遍历堆栈的处理程序链,调用每个处理程序 两次。

    • 补偿 SEH、C++ 和托管之间的不匹配 例外。

    • 分配托管异常实例并运行其构造函数。 最有可能的是,这涉及查找 各种错误的资源 消息。

    • 可能需要通过操作系统内核。经常拿一个硬件 例外。

    • 通知任何附加的调试器、分析器、向量异常处理程序 和其他相关方。

    这是光年之外 从您的函数返回 -1 称呼。异常是天生的 非本地的,如果有明显的 和今天的持久趋势 架构,这是你必须的 保持本地化以获得良好的性能。

    有些人会声称异常不是问题,没有性能问题,通常是一件好事。这些人得到了很多普选票,但他们完全错了。我见过微软员工提出同样的主张(通常是市场部保留的技术知识),但从马口中得出的底线是要谨慎使用它们。

    古老的格言,异常只能用于特殊情况,这对于 C# 和任何其他语言都是如此。

    【讨论】:

    • “尽量不要使用它们”?这是对布鲁姆建议的严重歪曲。
    • 不,这是我的建议,是让人们思考他们在做什么的概括。 Chris 说要使用它们,但非常谨慎:“......通过确保错误情况非常罕见”
    • 所以.....您建议检查这些值,而不是只使用“随便”的异常,所以您的建议不是“尽量不要使用异常”,而是更多“尝试不要创建例外”?
    • 没错,尽量不要使用异常。我没有说“尽量不要使用异常处理”。
    • 我会重新表述“异常很慢,尽量不要使用它们。”到“滥用异常的代码很糟糕,不要这样做。不要将异常用于正常的程序流程。对于真正的异常情况,谁在乎速度?”
    猜你喜欢
    • 1970-01-01
    • 2010-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-04
    • 2011-04-09
    • 1970-01-01
    相关资源
    最近更新 更多