51

我听过这样的建议,如果可能的话,你应该避免尝试 catch 块,因为它们很昂贵。

我的问题是关于 .NET 平台的:为什么 try 块很贵?

回复摘要:

在这个问题上显然有两个阵营:那些说尝试块很昂贵,而那些说“可能有点贵”。

那些说 try 块很昂贵的人通常会提到展开调用堆栈的“高成本”。就个人而言,我不相信这个论点 - 特别是在阅读了异常处理程序如何存储在这里之后。

Jon Skeet 属于“也许有一点点”阵营,他写了两篇关于异常和性能的文章,您可以在此处找到。

有一篇文章我觉得非常有趣:它谈到了 try 块的“其他”性能影响(不一定是内存或 cpu 消耗)。Peter Ritchie 提到他发现 try 块中的代码没有像编译器那样优化。你可以在这里阅读他的发现。

最后,在 CLR 中实现异常的人有一篇关于该问题的博客文章。去看看 Chris Brumme 的文章

4

11 回答 11

20

昂贵的不是块本身,甚至没有捕获异常本身,这是昂贵的,它是运行时展开调用堆栈,直到找到可以处理异常的堆栈帧。抛出异常是相当轻量级的,但如果运行时必须遍历六个堆栈帧(即六个方法调用深度)才能找到合适的异常处理程序,可能会在执行过程中执行 finally 块,您可能会看到明显的时间流逝.

于 2008-10-02T21:12:47.797 回答
14

您不应该避免 try/catch 块,因为这通常意味着您没有正确处理可能发生的异常。结构化异常处理 (SEH) 仅在异常实际发生时成本高昂,因为运行时必须遍历调用堆栈以查找 catch 处理程序,执行该处理程序(可能有多个),然后执行 finally 块,然后返回控制回到正确位置的代码。

异常并非旨在用于控制程序逻辑,而是用于指示错误情况。

关于异常的最大误解之一是它们是针对“异常情况”的。实际情况是它们用于传达错误条件。从框架设计的角度来看,没有所谓的“异常情况”。条件是否异常取决于使用的上下文, --- 但可重用库很少知道它们将如何使用。例如,OutOfMemoryException 对于简单的数据输入应用程序可能是异常的;对于执行自己的内存管理的应用程序(例如 SQL 服务器)来说,这并不是什么特别的事情。换句话说,一个人的特殊情况是另一个人的慢性病。 [http://blogs.msdn.com/kcwalina/archive/2008/07/17/ExceptionalError.aspx]

于 2008-10-02T21:18:09.763 回答
6

一个 try 块一点也不贵。除非抛出异常,否则几乎不会产生任何成本。如果抛出异常,那是一种特殊情况,您不再关心性能。如果您的程序需要 0.001 秒或 1.0 秒才能结束,这有关系吗?不,不是的。重要的是报告给您的信息有多好,以便您可以修复它并阻止它再次发生。

于 2009-02-03T15:36:55.680 回答
4

我认为人们真的高估了抛出异常的性能成本。是的,性能受到影响,但相对较小。

我运行了以下测试,抛出并捕获了一百万个异常。在我的 Intel Core 2 Duo 2.8 GHz 上花了大约 20 秒。每秒大约有 50K 异常。如果你只扔了一小部分,你就会遇到一些架构问题。

这是我的代码:

using System;
using System.Diagnostics;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < 1000000; i++)
            {
                try
                {
                    throw new Exception();
                }
                catch {}
            }
            Console.WriteLine(sw.ElapsedMilliseconds);
            Console.Read();
        }
    }
}
于 2008-10-03T01:38:49.310 回答
3

当您将代码包装在 try/catch 块中时,编译器会发出更多 IL;看,对于以下程序:

using System;
public class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("abc");
    }
}

编译器会发出这个 IL:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       13 (0xd)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "abc"
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ret
} // end of method Program::Main

而对于稍作修改的版本:

using System;
public class Program
{
    static void Main(string[] args)
    {
        try { Console.WriteLine("abc"); }
        catch { }
    }
}

发出更多:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       23 (0x17)
  .maxstack  1
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "abc"
    IL_0007:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000c:  nop
    IL_000d:  nop
    IL_000e:  leave.s    IL_0015
  }  // end .try
  catch [mscorlib]System.Object 
  {
    IL_0010:  pop
    IL_0011:  nop
    IL_0012:  nop
    IL_0013:  leave.s    IL_0015
  }  // end handler
  IL_0015:  nop
  IL_0016:  ret
} // end of method Program::Main

所有这些 NOP 和其他成本。

于 2008-10-04T11:29:42.180 回答
3

IMO 整个讨论就像是在说“哇 lops 很贵,因为我需要增加一个计数器……我不会再使用它们了”,或者“哇创建一个对象需要时间,我不会创建不再有成吨的物体了。”

底线是您添加代码,大概是有原因的。如果代码行没有产生一些开销,即使它有 1 个 CPU 周期,那它为什么会存在呢?没有什么是免费的。

与您添加到应用程序中的任何代码行一样,明智的做法是仅在需要它做某事时才将其放在那里。如果捕获异常是您需要做的事情,那么就这样做......就像如果您需要一个字符串来存储某些内容,请创建一个新字符串。同样,如果您声明一个从未使用过的变量,那么您就是在浪费内存和 CPU 周期来创建它,应该将其删除。与尝试/捕获相同。

换句话说,如果有代码可以做某事,那么假设做某事会以某种方式消耗 CPU 和/或内存。

于 2009-02-20T21:11:44.523 回答
2

您需要担心的不是 try 块,而是catch块。然后,并不是您要避免编写这些块:而是您希望尽可能多地编写永远不会真正使用它们的代码。

于 2008-10-02T21:23:43.510 回答
1

这不是我永远担心的事情。我宁愿关心尝试的清晰度和安全性……最后不要担心自己有多“昂贵”。

我个人不使用 286,也没有人使用.NET或 Java。继续前行。担心编写会影响您的用户和其他开发人员的好代码,而不是为 99.999999% 的使用它的人正常工作的底层框架。

这可能不是很有帮助,我并不是要严厉,而只是强调观点。

于 2008-10-03T00:02:21.257 回答
1

我怀疑它们是否特别昂贵。很多时候,它们是必要的/必需的。

尽管我强烈建议仅在必要时在正确的位置/嵌套级别使用它们,而不是在每次调用返回时重新抛出异常。

我想这个建议的主要原因是说你不应该在 if---else 是更好的方法的地方使用 try-catch。

于 2008-10-02T21:12:50.323 回答
0

有点 O/T,但是...

有相当好的设计概念说你永远不应该要求异常处理。这意味着您应该能够在任何对象被抛出异常之前查询任何可能抛出异常的条件。

就像能够在“write()”之前说“writable()”,诸如此类。

这是一个不错的主意,如果使用它,它会使 Java 中的已检查异常看起来有点愚蠢——我的意思是,检查一个条件,然后就被迫仍然为相同的条件编写一个 try/catch?

这是一个非常好的模式,但是编译器可以强制执行检查异常,这些检查不能。此外,并非所有库都是使用这种设计模式创建的——当您考虑异常时,请牢记这一点。

于 2008-10-02T21:26:46.217 回答
0

每次尝试都需要记录很多信息,例如堆栈指针、CPU 寄存器的值等,以便在抛出异常时可以展开堆栈并恢复通过try块时的状态。不仅每次尝试都需要记录很多信息,当抛出异常时,需要恢复很多值。所以尝试非常昂贵,而投掷/接球也非常昂贵。

这并不意味着您不应该使用异常,但是,在性能关键代码中,您可能不应该使用太多的尝试,也不应该过于频繁地抛出异常。

于 2008-10-02T21:39:28.653 回答