当不抛出exception时,是否尝试/捕获块会损害性能?

在与微软员工进行代码审查的过程中,我们遇到了try{}块中的大部分代码。 她和IT代表build议,这可能会影响代码的执行。 实际上,他们build议大部分的代码应该在try / catch块之外,只有重要的部分应该被检查。 微软员工补充说,即将发布的白皮书警告不正确的try / catch块。

我环顾四周,发现它可以影响优化 ,但似乎只适用于范围之间共享variables。

我并不是在询问代码的可维护性,甚至是处理正确的exception(这个代码无疑需要重新分解)。 我也不是指使用exception进行stream量控制,这在大多数情况下显然是错误的。 这些是重要的问题(有些更重要),但不是重点。

try / catch块在抛出exception时如何影响性能?

编辑:我添加一个赏金。 有一些有趣的回应,但我想获得更多的意见。

核实。

 static public void Main(string[] args) { Stopwatch w = new Stopwatch(); double d = 0; w.Start(); for (int i = 0; i < 10000000; i++) { try { d = Math.Sin(1); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } w.Stop(); Console.WriteLine(w.Elapsed); w.Reset(); w.Start(); for (int i = 0; i < 10000000; i++) { d = Math.Sin(1); } w.Stop(); Console.WriteLine(w.Elapsed); } 

输出:

 00:00:00.4269033 // with try/catch 00:00:00.4260383 // without. 

以毫秒为单位:

 449 416 

新代码:

 for (int j = 0; j < 10; j++) { Stopwatch w = new Stopwatch(); double d = 0; w.Start(); for (int i = 0; i < 10000000; i++) { try { d = Math.Sin(d); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { d = Math.Sin(d); } } w.Stop(); Console.Write(" try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); w.Reset(); d = 0; w.Start(); for (int i = 0; i < 10000000; i++) { d = Math.Sin(d); d = Math.Sin(d); } w.Stop(); Console.Write("No try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); Console.WriteLine(); } 

新的结果:

  try/catch/finally: 382 No try/catch/finally: 332 try/catch/finally: 375 No try/catch/finally: 332 try/catch/finally: 376 No try/catch/finally: 333 try/catch/finally: 375 No try/catch/finally: 330 try/catch/finally: 373 No try/catch/finally: 329 try/catch/finally: 373 No try/catch/finally: 330 try/catch/finally: 373 No try/catch/finally: 352 try/catch/finally: 374 No try/catch/finally: 331 try/catch/finally: 380 No try/catch/finally: 329 try/catch/finally: 374 No try/catch/finally: 334 

看到所有的try / catch和没有try / catch的统计数据后,好奇心迫使我往后看,看看这两种情况产生了什么。 这里是代码:

C#:

 private static void TestWithoutTryCatch(){ Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); } 

MSIL:

 .method private hidebysig static void TestWithoutTryCatch() cil managed { // Code size 32 (0x20) .maxstack 8 IL_0000: nop IL_0001: ldstr "SIN(1) = {0} - No Try/Catch" IL_0006: ldc.r8 1. IL_000f: call float64 [mscorlib]System.Math::Sin(float64) IL_0014: box [mscorlib]System.Double IL_0019: call void [mscorlib]System.Console::WriteLine(string, object) IL_001e: nop IL_001f: ret } // end of method Program::TestWithoutTryCatch 

C#:

 private static void TestWithTryCatch(){ try{ Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); } catch (Exception ex){ Console.WriteLine(ex); } } 

MSIL:

 .method private hidebysig static void TestWithTryCatch() cil managed { // Code size 49 (0x31) .maxstack 2 .locals init ([0] class [mscorlib]System.Exception ex) IL_0000: nop .try { IL_0001: nop IL_0002: ldstr "SIN(1) = {0}" IL_0007: ldc.r8 1. IL_0010: call float64 [mscorlib]System.Math::Sin(float64) IL_0015: box [mscorlib]System.Double IL_001a: call void [mscorlib]System.Console::WriteLine(string, object) IL_001f: nop IL_0020: nop IL_0021: leave.s IL_002f //JUMP IF NO EXCEPTION } // end .try catch [mscorlib]System.Exception { IL_0023: stloc.0 IL_0024: nop IL_0025: ldloc.0 IL_0026: call void [mscorlib]System.Console::WriteLine(object) IL_002b: nop IL_002c: nop IL_002d: leave.s IL_002f } // end handler IL_002f: nop IL_0030: ret } // end of method Program::TestWithTryCatch 

我不是IL的专家,但是我们可以看到,在第四行.locals init ([0] class [mscorlib]System.Exception ex)创build了一个局部exception对象,事情和没有try /赶上17 IL_0021: leave.s IL_002f线IL_0021: leave.s IL_002f 。 如果发生exception,则控制跳转到IL_0025: ldloc.0IL_0025: ldloc.0否则我们跳转到标签IL_002d: leave.s IL_002f并返回函数。

我可以安全地假设,如果没有发生exception,那么创build局部variables用于保存exception对象和跳转指令是开销。

不可以。如果try / finally块排除的微小优化实际上会对程序产生可测量的影响,那么您可能不应该首先使用.NET。

.NETexception模型的完整的解释。

Rico Mariani的performance花絮: 例外花费:什么时候投掷,何时不投掷

第一种成本是在代码中进行exception处理的静态成本。 在这里,被pipe理的exception事实上做得相当好,我的意思是说静态成本可以比C ++中低很多。 为什么是这样? 那么,静态成本实际上是在两种地方产生的:一是try / finally / catch / throw这些构造的代码的实际位置。 其次,在unmanged代码中,隐藏成本与追踪所有必须在抛出exception的情况下被破坏的对象有关。 有相当数量的清理逻辑必须存在,而且鬼鬼祟祟的部分是,即使是本身没有抛出或捕获或以其他方式公开使用exception的代码,仍然承担着知道如何自行清理的负担。

Dmitriy Zaslavskiy:

根据克里斯·布鲁姆(Chris Brumme)的说法:还有一个成本与一些优化没有被JIT执行的情况有关

本M的例子中的结构是不同的。 它会在内部for循环内部被扩展开for ,这将导致它不能很好的比较两种情况。

以下是比较准确的比较,其中要检查的整个代码(包括variables声明)在Try / Catch块中:

  for (int j = 0; j < 10; j++) { Stopwatch w = new Stopwatch(); w.Start(); try { double d1 = 0; for (int i = 0; i < 10000000; i++) { d1 = Math.Sin(d1); d1 = Math.Sin(d1); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { //d1 = Math.Sin(d1); } w.Stop(); Console.Write(" try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); w.Reset(); w.Start(); double d2 = 0; for (int i = 0; i < 10000000; i++) { d2 = Math.Sin(d2); d2 = Math.Sin(d2); } w.Stop(); Console.Write("No try/catch/finally: "); Console.WriteLine(w.ElapsedMilliseconds); Console.WriteLine(); } 

当我运行Ben M的原始testing代码时,我注意到了Debug和Releasconfiguration的差异。

这个版本,我注意到debugging版本(实际上比其他版本更多)的差异,但在发行版本没有区别。

结论
基于这些testing,我认为我们可以说Try / Catch对性能影响不大。

编辑:
我试图将循环值从1000万增加到10亿,并且在Release中再次运行以获得版本中的一些差异,结果是:

  try/catch/finally: 509 No try/catch/finally: 486 try/catch/finally: 479 No try/catch/finally: 511 try/catch/finally: 475 No try/catch/finally: 477 try/catch/finally: 477 No try/catch/finally: 475 try/catch/finally: 475 No try/catch/finally: 476 try/catch/finally: 477 No try/catch/finally: 474 try/catch/finally: 475 No try/catch/finally: 475 try/catch/finally: 476 No try/catch/finally: 476 try/catch/finally: 475 No try/catch/finally: 476 try/catch/finally: 475 No try/catch/finally: 474 

你看到的结果是不一致的。 在某些情况下,使用Try / Catch的版本实际上更快!

我在一个紧密的循环中testing了一个try..catch的实际影响,在任何正常的情况下,它本身就是一个性能问题。

如果循环做了很less的工作(在我的testing中,我做了一个x++ ),你可以测量exception处理的影响。 具有exception处理的循环花费了大约十倍的时间来运行。

如果循环做了一些实际的工作(在我的testing中,我调用了Int32.Parse方法),exception处理的影响太小,无法衡量。 通过交换循环顺序,我得到了更大的差异…

尝试catch块对性能的影响可以忽略不计,但exception投掷可以相当大,这可能是你的同事困惑的地方。

try / catch对性能有影响。

但是这不是一个巨大的影响。 try / catch的复杂性通常是O(1),就像一个简单的赋值,除非它们被放置在一个循环中。 所以你必须明智地使用它们。

这里有一个关于try / catch性能的参考(虽然没有解释它的复杂性,但它是隐含的)。 看看抛出更less的exception部分

理论上,除非实际发生exception,try / catch块将不会影响代码行为。 然而,在一些罕见的情况下,try / catch块的存在可能会产生重大的影响,还有一些非常罕见但却很难模糊的效应可以被注意到。 原因是给定的代码如下:

 Action q; double thing1() { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;} double thing2() { q=null; return 1.0;} ... x=thing1(); // statement1 x=thing2(x); // statement2 doSomething(x); // statement3 

编译器可以基于statement2保证在statement3之前执行的事实来优化statement1。 如果编译器可以识别出thing1没有副作用,而thing2实际上并不使用x,那么可以完全省略thing1。 如果[在这种情况下]东西很贵,这可能是一个主要的优化,尽pipe东西1昂贵的情况也是那些编译器最不可能优化的东西。 假设代码被改变了:

 x=thing1(); // statement1 try { x=thing2(x); } // statement2 catch { q(); } doSomething(x); // statement3 

现在存在一系列的事件,statement3可以执行而statement2没有被执行。 即使thing2的代码中没有任何东西可以抛出exception,也可能是另一个线程可能使用Interlocked.CompareExchange来注意到q被清除并将其设置为Thread.ResetAbort ,然后执行Thread.Abort() statement2将其值写入x 。 然后catch会执行Thread.ResetAbort() [通过委托q ],允许执行继续statement3。 这样的一系列事件当然是非常不可能的,但是需要编译器来生成符合规范的代码,即使发生这种不可能的事件。

一般来说,编译器更容易注意到比简单代码less的代码,因此try / catch很less会影响性能,如果从不抛出exception。 尽pipe如此,在某些情况下,try / catch块的存在可能会阻止优化,但是对于try / catch而言,这会使代码运行得更快。

关于try / catch实现的讨论,讨论try / catch块如何工作,以及一些实现如何具有高开销,有些没有任何exception发生时的零开销。 特别是,我认为Windows 32位实现有很高的开销,而64位实现没有。