关于.net:为什么尝试块很贵?

Why are try blocks expensive?

我听过这样的建议:如果可能的话,你应该尽量避免使用挡块,因为它们很贵。

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

答复摘要:

在这个问题上,显然有两个阵营:一个说试块很贵,另一个说"可能有一点点"。

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

乔恩·斯基特坐在"也许有点小"阵营里,写了两篇关于例外和性能的文章,你可以在这里找到。

有一篇文章我觉得非常有趣:它讨论了Try块的"其他"性能影响(不一定是内存或CPU消耗)。PeterRitchie提到,他发现try块中的代码没有被优化,否则编译器会对其进行优化。你可以在这里了解他的发现。

最后,还有一个关于这个问题的博客条目,来自在clr中实现异常的人。去看看克里斯·布鲁姆的文章。


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


您不应该避免Try/Catch块,因为这通常意味着您没有正确地处理可能发生的异常。只有当异常实际发生时,结构化异常处理(seh)才是昂贵的,因为运行时必须遍历调用堆栈查找catch处理程序,执行该处理程序(可能不止一个),然后执行finally块,然后将控制权返回到正确位置的代码。

异常不是用来控制程序逻辑,而是用来指示错误情况。

One of the biggest misconceptions
about exceptions is that they are for
"exceptional conditions." The reality
is that they are for communicating
error conditions. From a framework
design perspective, there is no such
thing as an"exceptional condition".
Whether a condition is exceptional or
not depends on the context of usage,
--- but reusable libraries rarely know how they will be used. For example,
OutOfMemoryException might be
exceptional for a simple data entry
application; it’s not so exceptional
for applications doing their own
memory management (e.g. SQL server).
In other words, one man’s exceptional
condition is another man’s chronic
condition.
[http://blogs.msdn.com/kcwalina/archive/2008/07/17/ExceptionalError.aspx]


试块一点也不贵。除非抛出异常,否则所产生的成本很小或没有。如果抛出了一个异常,那就是一个异常情况,您不再关心性能了。如果你的程序需要0.001秒或1.0秒才能完成,这是否重要?不,不是。重要的是,向您报告的信息有多好,这样您就可以修复它并阻止它再次发生。


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

我进行了以下测试,抛出并捕获了一百万个异常。我的Intel Core 2 Duo 2.8 GHz用了大约20秒。这大约是每秒5万个例外。如果你只扔了其中的一小部分,你会遇到一些架构问题。

以下是我的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
        }
    }
}


在我看来,整个讨论就像是说"哇,lops很贵,因为我需要增加一个计数器…我不会再使用它们了,或者"哇,创建一个对象需要时间,我不会再创建大量的对象。"

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

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

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


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

1
2
3
4
5
6
7
8
using System;
public class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("abc");
    }
}

编译器将发出此IL:

1
2
3
4
5
6
7
8
9
10
11
.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

对于稍微修改过的版本:

1
2
3
4
5
6
7
8
9
using System;
public class Program
{
    static void Main(string[] args)
    {
        try { Console.WriteLine("abc"); }
        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
.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和其他费用。


它不是试块,你需要担心的就跟抓块一样多。然后,并不是你想避免编写代码块:而是你想尽可能多地编写永远不会实际使用它们的代码。


这不是我担心的事。我宁愿关心一次尝试的清晰性和安全性……最后,不要再为自己担心它有多"昂贵"。

我个人不使用286,也没有任何人使用.NET或Java。继续前进。担心编写好的代码会影响到您的用户和其他开发人员,而不是对99.999999%使用它的人都有效的底层框架。

这可能不是很有帮助,我不是刻薄而是强调观点。


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

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

我想这个建议的主要原因是说你不应该在"如果——否则会是更好的方法"的地方使用"尝试捕获"。


每次尝试都需要记录大量信息,例如堆栈指针、CPU寄存器的值等,以便在引发异常时释放堆栈并恢复传递尝试块时的状态。不仅每次尝试都需要记录大量的信息,当抛出异常时,还需要恢复大量的值。所以一次尝试是非常昂贵的,一次投掷/接球也是非常昂贵的。

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


有点O/T,但是……

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

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

这是一个不错的主意,如果使用,它会使Java中的检查异常变得愚蠢——我的意思是,检查一个条件,然后在那之后,还是被迫为相同的条件写一个TIG/catch?

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