Do try/catch blocks hurt performance when exceptions are not thrown?
在与一位微软员工进行代码审查时,我们在一个
我环顾四周,发现它会影响优化,但它似乎只适用于范围之间共享变量的情况。
我不是在问代码的可维护性,甚至不是在处理正确的异常(毫无疑问,所讨论的代码需要重新分解)。我也不是指使用异常进行流控制,这在大多数情况下显然是错误的。这些是重要的问题(有些更重要),但不是重点。
当不引发异常时,try/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 26 27 28 29 30 31 32 | 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); } |
输出:
1 2 | 00:00:00.4269033 // with try/catch 00:00:00.4260383 // without. |
以毫秒为单位:
1 2 | 449 416 |
新代码:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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(); } |
新结果:
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 26 27 28 29 | 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和without try/catch的统计数据之后,好奇心迫使我向后看,看看这两种情况都产生了什么。代码如下:
C:
1 2 3 | private static void TestWithoutTryCatch(){ Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); } |
MSIL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .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:
1 2 3 4 5 6 7 8 | private static void TestWithTryCatch(){ try{ Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); } catch (Exception ex){ Console.WriteLine(ex); } } |
MSIL:
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 26 27 28 29 30 31 32 | .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的专家,但我们可以看到,在第四行
我可以安全地假设,如果没有发生异常,那么创建局部变量来保存异常对象的开销就是only->strike>和跳转指令。
不。如果一个Try/Finally块排除的一些微不足道的优化实际上对您的程序产生了可测量的影响,那么您可能不应该首先使用.NET。
对.NET异常模型的全面解释。
里科·马里亚尼的表演小道消息:例外成本:什么时候扔,什么时候不扔
The first kind of cost is the static
cost of having exception handling in
your code at all. Managed exceptions
actually do comparatively well here,
by which I mean the static cost can be
much lower than say in C++. Why is
this? Well, static cost is really
incurred in two kinds of places:
First, the actual sites of
try/finally/catch/throw where there's
code for those constructs. Second, in
unmanged code, there's the stealth
cost associated with keeping track of
all the objects that must be
destructed in the event that an
exception is thrown. There's a
considerable amount of cleanup logic
that must be present and the sneaky
part is that even code that doesn't
itself throw or catch or otherwise
have any overt use of exceptions still
bears the burden of knowing how to
clean up after itself.
德米特里·扎斯拉夫斯基:
As per Chris Brumme's note: There is
also a cost related to the fact the
some optimization are not being
performed by JIT in the presence of
catch
这个例子中的结构与ben m不同,它将在内部
当要检查的整个代码(包括变量声明)位于try/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 26 27 28 29 30 31 32 | 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运行最初的测试代码时,我注意到debug和releas配置都有不同。
在这个版本中,我注意到了调试版本的不同(实际上比其他版本要多),但在发布版本中没有区别。
结论:基于这些测试,我认为我们可以说try/catch对性能的影响很小。
编辑:我尝试将循环值从10000000增加到100000000,然后在版本中再次运行以获得版本中的一些差异,结果是:
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 26 27 28 29 | 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的版本实际上更快!
我在一个紧密的循环中测试了
如果循环几乎不起作用(在我的测试中,我做了一个
如果循环做了一些实际工作(在我的测试中,我调用了int32.parse方法),异常处理的影响太小,无法测量。我通过交换循环的顺序得到了更大的区别…
Try-Catch块对性能的影响微乎其微,但异常抛出可能相当大,这可能是您的同事感到困惑的地方。
尝试/捕获对性能有影响。
但影响不大。Try/Catch的复杂性通常是O(1),就像一个简单的赋值,除非它们被放置在一个循环中。所以你必须明智地使用它们。
这里有一个关于Try/Catch性能的参考(虽然没有解释它的复杂性,但它是隐含的)。看看抛出更少的异常部分
理论上,除非实际发生异常,否则try/catch块不会影响代码行为。然而,在一些罕见的情况下,尝试/捕获块的存在可能会产生重大影响,而在一些不常见但几乎不模糊的情况下,效果可能会明显。原因是给定的代码如下:
1 2 3 4 5 6 7 8 9 | 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 |
编译器可以根据保证语句2在语句3之前执行的事实来优化语句1。如果编译器能够识别出thing1没有副作用,thing2实际上没有使用x,那么它可以安全地完全忽略thing1。如果[在本例中]thing1很昂贵,这可能是一个主要的优化,尽管thing1很昂贵的情况也是编译器最不可能优化的情况。假设代码已更改:
1 2 3 4 5 | x=thing1(); // statement1 try { x=thing2(x); } // statement2 catch { q(); } doSomething(x); // statement3 |
现在存在一系列事件,其中Statement3可以在未执行Statement2的情况下执行。即使
一般来说,编译器更容易注意到遗漏简单代码位的机会,而不是遗漏复杂代码位的机会,因此,如果不抛出异常,则很少会出现try/catch会影响性能的情况。尽管如此,在某些情况下,存在try/catch块可能会阻止优化(但对于try/catch而言),从而使代码运行更快。
请参阅关于Try/Catch实现的讨论,以了解Try/Catch块如何工作,以及某些实现的开销如何很大,而有些实现的开销为零,当没有异常发生时。特别是,我认为Windows32位实现有很高的开销,而64位实现没有。