2008-10-02 64 views
47

我听说过你应该避免尝试catch块的建议,因为它们很贵。为什么试块价格昂贵?

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

回应摘要:

显然有关于这个问题的两个阵营:那些说try块是昂贵的,那些说“也许一个小小的一点”。

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

乔恩Skeet坐在“也许是一个小小的”阵营,并写了两篇关于例外和性能的文章,你可以找到here

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

最后,还有一个关于在CLR中实现异常的人的博客条目。去看看克里斯布鲁姆的文章here

回答

20

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

+4

另外,扫描调试数据以解析打印堆栈跟踪的方法名称和行号会增加很多开销。 – 2008-10-03 00:05:48

+0

非常真实。根据.pdb文件的存在与否,您会看到很大的差异。 – 2008-10-03 03:07:19

1

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

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

我会想象的建议的主要原因是说,你不应该使用try-catches,如果--- else将是一个更好的方法。

12

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

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

其中一个最大的误解 约例外的是,他们是 “得天独厚的条件。”现实 是,他们传达 错误条件。从框架 的设计角度来看,没有这样的东西作为“特殊条件”。 一个条件是例外还是 不取决于使用的上下文, ---但可重用的库很少知道它们将如何使用。例如, OutOfMemoryException可能是 例外,对于简单数据条目 应用程序; 对于自己的 内存管理(例如SQL服务器)的应用程序并不是特别的例外。 换句话说,一个人的特殊 条件是另一个男人的慢性 条件。 [http://blogs.msdn.com/kcwalina/archive/2008/07/17/ExceptionalError.aspx]

2

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

0

略O/T,但是...

有相当不错的设计理念是说,你不应该需要异常处理。这意味着您应该能够查询任何对象,以查找在抛出异常之前可能抛出异常的任何条件。

就像在write()之前能够说“可写()”一样。

这是一个体面的想法,如果使用它,它使Java中的检查异常看起来很愚蠢 - 我的意思是,检查一个条件并在此之后,被迫仍然为相同的条件编写try/catch?

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

+1

在很多情况下,竞赛条件不允许这种预检工作。 – erickson 2008-10-02 23:25:55

+0

可能,但我不能拿出一个不涉及外部系统的例子。 – 2008-10-03 17:13:41

0

每个尝试需要记录大量信息,例如,堆栈指针,CPU寄存器值等,以便在抛出异常的情况下,它可以展开堆栈并返回通过尝试块时的状态。不仅如此,每个尝试都需要记录大量信息,当抛出异常时,需要恢复大量值。所以尝试是非常昂贵的和扔/ catch也是非常昂贵的。

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

+0

但是我做了一个基准测试,在那里我调用了没有try/catch块的100万次Java方法。然后我调用了一个确切的方法,也是100万次,但它有一个try/catch块(但从未抛出)。在纳秒测量时,我无法观察到任何显着差异。我已经得出结论,要输入一个try块,如果它确实抓住了当前状态的快照,那么它可以忽略不计。只有投掷部分是昂贵的。你为什么说“尝试非常昂贵[...]”? – Jeach 2010-08-26 14:31:08

+0

@Jeach:因为是这样。看到我对这个问题的回答,我自己发布基准代码和基准测试结果。 http://stackoverflow.com/questions/299068/how-slow-are-java-exceptions/299315#299315 - 今天它不再是非常昂贵,因为CPU速度更快,并且最新的JVM具有高度优化的方式消除在运行时永远不会抛出异常的尝试 - 但尝试在旧CPU上使用旧的JVM,并且您会看到它们的成本如何。 Java是跨平台的,你永远不知道什么目标机器有一天会运行你的代码或什么JVM版本(除非你强制最新)。 – Mecki 2010-08-27 15:00:04

1

这不是我永远不会担心的事情。我宁愿关心一次尝试的清晰度和安全性......最后会阻止我自己如何“昂贵”。

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

这可能不是很有帮助,我不是故意尖刻,而是突出视角。

5

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

我跑了下面的测试,抛出了100万个例外。我的英特尔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(); 
     } 
    } 
} 
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

尝试块并不昂贵。除非抛出异常,否则几乎没有成本发生。如果抛出异常,这是一种特殊的情况,你不再关心性能。如果您的程序需要0.001秒或1.0秒才能倒下,这有什么关系?不,不是的。重要的是,向您报告的信息有多好,以便您能够修复并再次停止它。

3

海事组织这整个讨论就像说:“哇,因为我需要增加一个计数器......我不会再使用它们了”,或者“创建一个对象需要时间,我是不会再造成大量物体了。“

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

明智的做法,就像您添加到应用程序的任何代码行一样,只有在需要它执行某些操作时才会将其放在那里。如果捕获一个异常是你需要做的事情,那就做吧......就像你需要一个字符串来存储一些东西,创建一个新的字符串一样。同样的方法,如果你声明一个以前没有使用过的变量,那么你正在浪费内存和CPU周期来创建它,它应该被删除。与try/catch一样。

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