为什么试块价格昂贵?
我听说过这样的建议:如果可能,应避免尝试使用catch块,因为它们很昂贵。
我的问题特别是关于.NET平台的:为什么try块昂贵?
回应摘要:
在这个问题上显然有两个阵营:那些说尝试块很昂贵的阵营,和那些说"也许一点点"的阵营。
那些说尝试块很昂贵的人通常会提到取消调用堆栈的"高成本"。就个人而言,特别是在阅读了有关异常处理程序在此处存储的方式之后,我并没有对此参数感到信服。
乔恩·斯基特(Jon Skeet)坐在"也许只是一点点"阵营,并写了两篇关于异常和性能的文章,我们可以在这里找到。
我发现有一篇文章非常有趣:它谈到了try块的"其他"性能影响(不一定是内存或者cpu消耗)。彼得·里奇(Peter Ritchie)提到,他发现try块中的代码并未得到优化,否则编译器会对其进行优化。我们可以在这里阅读有关他的发现的信息。
最后,在CLR中实现异常的人中有一个关于该问题的博客条目。在这里看看克里斯·布鲁姆(Chris Brumme)的文章。
解决方案
并不是块本身很昂贵,它甚至没有捕获异常本身,这很昂贵,是运行时取消调用堆栈,直到找到可以处理异常的堆栈框架。抛出异常的重量很轻,但是如果运行时必须向上移动六个堆栈帧(即深度调用六个方法)以找到合适的异常处理程序,并可能最终执行finally块,则可能会看到明显的时间流。
我怀疑它们是否特别昂贵。很多时候,它们是必需的/必需的。
尽管我强烈建议仅在必要时在正确的嵌套位置/级别使用它们,而不是在每次调用返回时都抛出异常。
我想该建议的主要原因是要说我们不应该使用try-catches,否则if --- else将是更好的方法。
我们不应该避免使用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]
它不是尝试块,而是我们需要担心的捕获块。然后,并不是我们要避免编写这些块:而是要尽可能多地编写永远不会实际使用它们的代码。
O / T有点,但是...
有一个相当不错的设计概念,说我们永远都不需要异常处理。这只是意味着我们应该能够在引发异常之前查询任何对象以查找可能引发异常的任何条件。
就像能够在" write()"之前说" writable()"之类的东西一样。
这是一个不错的主意,如果使用的话,它会使Java中的检查异常看起来很愚蠢-我的意思是,检查条件并在此之后立即被迫仍然为相同条件编写try / catch?
这是一个很好的模式,但是检查异常可以由编译器强制执行,而这些检查则不能。同样,并非所有库都是使用这种设计模式制作的-当我们在考虑异常时,要牢记这一点。
每次尝试都需要记录很多信息,例如堆栈指针,CPU寄存器的值等,因此它可以展开堆栈并返回通过try块时的状态,以防万一引发异常。不仅每次尝试都需要记录很多信息,而且在引发异常时,还需要恢复很多值。因此,尝试非常昂贵,掷/接球也非常昂贵。
这并不意味着我们不应该使用异常,但是,在对性能有严格要求的代码中,我们不应使用过多的尝试,也不要过于频繁地抛出异常。
这是我永远不会担心的事情。我宁愿尝试一下它的清晰度和安全性……最后以"昂贵"的价格来阻止自己。
我个人不使用286,也没有人使用.NET或者Java。继续。担心编写会影响用户和其他开发人员的良好代码,而不是对99.999999%的使用该代码的人有效的基础框架。
这可能不是很有帮助,我并不是要刻薄,而只是强调观点。
我认为人们确实高估了引发异常的性能成本。是的,虽然对性能有影响,但是影响相对较小。
我进行了以下测试,引发并捕获了100万个异常。我的Intel Core 2 Duo(2.8 GHz)花了大约20秒钟。每秒大约有5万个例外。如果只投入其中的一小部分,就会遇到一些体系结构问题。
这是我的代码:
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(); } } }
将代码包装在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和其他成本。
一个try块并不昂贵。除非引发异常,否则几乎不会产生任何费用。如果抛出了异常,那是一种特殊情况,我们不再关心性能。程序需要0.001秒还是1.0秒的过渡时间有关系吗?不,不是的。重要的是报告回给信息有多好,因此我们可以修复它并阻止它再次发生。
IMO整个讨论就像是说"哇,因为我需要增加一个计数器,所以价格昂贵……我不再使用它们了",或者"哇,创建一个对象要花时间,我不会去创建。一吨的东西了。"
底线是我们添加的代码,大概是有原因的。如果代码行没有花一些时间(即使是1个CPU周期),那为什么会存在呢?没有什么是免费的。
与添加到应用程序中的任何代码行一样,明智的做法是仅在需要它执行某些操作时才将其放到那里。如果捕获异常是我们需要执行的操作,那么就执行此操作...就像我们需要一个字符串来存储某些内容一样,请创建一个新字符串。同样,如果声明一个从未使用过的变量,则浪费了创建它的内存和CPU周期,应将其删除。与try / catch相同。
换句话说,如果有代码要做某事,则假定做某事将以某种方式消耗CPU和/或者内存。