为什么尝试块很贵?

时间:2008-10-02 21:08:10

标签: .net coding-style

我听说过,如果可能的话,你应该避免尝试使用catch块,因为它们价格昂贵。

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

回复摘要:

在这个问题上显然有两个阵营:那些说试试块很贵的人,以及那些说“可能只是一点点”的阵营。

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

Jon Skeet坐在“可能是一点点”的阵营中,写了两篇关于异常和表现的文章,你可以找到here

我发现有一篇文章非常有趣:它谈到了try块的“其他”性能影响(不一定是内存或CPU消耗)。 Peter Ritchie提到他发现try块中的代码没有被优化,因为它本来是编译器。你可以阅读他的发现here

最后,有一篇博客文章介绍了在CLR中实现异常的人的问题。去看看Chris Brumme的文章here

11 个答案:

答案 0 :(得分:20)

块本身并不昂贵,而且它本身并不是捕获一个异常本身就是昂贵的,它是运行时展开调用堆栈直到它找到一个可以处理异常的堆栈帧。抛出异常是非常轻量级的,但是如果运行时必须向上走6个堆栈帧(即深度为6个方法调用)以找到适当的异常处理程序,可能会执行finally块,因此可能会看到明显的时间量

答案 1 :(得分:12)

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

异常不是用于控制程序逻辑,而是用于指示错误条件。

  

最大的误解之一   关于例外的是它们的用途   “特殊情况。”现实   是他们的沟通   错误条件。来自一个框架   设计角度来看,没有这样的   作为“特殊情况”的事情。   条件是否异常或   不取决于使用环境,   ---但是可重用的库很少知道如何使用它们。例如,   OutOfMemoryException可能是   简单的数据输入   应用;它不是那么特别   对于自己做的应用程序   内存管理(例如SQL服务器)。   换句话说,一个人是特殊的   病情是另一个男人的慢性病   条件。   [http://blogs.msdn.com/kcwalina/archive/2008/07/17/ExceptionalError.aspx]

答案 2 :(得分:5)

尝试块根本不贵。除非引发异常,否则很少或不产生任何费用。如果抛出异常,那就是异常情况,你不再关心性能了。如果您的程序需要0.001秒或1.0秒才能倒下,这是否重要?不,不是的。重要的是报告给你的信息有多好,这样你就可以修复它并阻止它再次发生。

答案 3 :(得分:4)

我认为人们真的高估了抛出异常的性能成本。是的,有一个性能损失,但它相对较小。

我进行了以下测试,投掷并捕获了一百万例外。我的英特尔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();
        }
    }
}

答案 4 :(得分: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和其他费用。

答案 5 :(得分:3)

IMO整个讨论就像是说“哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇将再创造大量物品。“

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

明智的做法,就像你添加到你的应用程序中的任何代码行一样,只要你需要它就可以把它放在那里。如果你需要做一些异常捕获,那就去做吧......就像你需要一个字符串来存储东西一样,创建一个新的字符串。通过相同的方法,如果声明一个未使用的变量,则会浪费内存和CPU周期来创建它,并且应该将其删除。与try / catch相同。

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

答案 6 :(得分:2)

这不是尝试阻止你需要担心 catch 块。然后,并不是你想要避免编写块:这是你想要尽可能多地编写永远不会实际使用它们的代码。

答案 7 :(得分:1)

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

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

我认为这个建议的主要原因是说你不应该使用try-catches if if else是一种更好的方法。

答案 8 :(得分:1)

这不是我会担心的事情。我宁愿关心尝试的清晰度和安全性......最终还是会对自己的“昂贵”感到不满。

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

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

答案 9 :(得分:0)

略有O / T,但是......

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

就像在“write()”之前能说“writable()”一样,就像那样。

这是一个不错的主意,如果使用它,它会使Java中的检查异常看起来很愚蠢 - 我的意思是,检查一个条件,然后在那之后,被迫仍然为同样的条件写一个try / catch? / p>

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

答案 10 :(得分:0)

每次尝试都需要记录大量信息,例如堆栈指针,CPU寄存器的值等等,因此它可以解除堆栈并恢复传递 try 块时的状态,以防抛出异常。不仅每个尝试都需要记录大量信息,当抛出异常时,需要恢复很多值。所以尝试非常昂贵, throw / catch 也非常昂贵。

这并不意味着您不应该使用异常,但是,在性能关键代码中,您可能不应该使用太多尝试而且也不会经常抛出异常。