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