在一次微软员工的代码检查中,我们在一个try{}块中发现了一大块代码。她和一位IT代表表示,这可能会对代码的性能产生影响。事实上,他们建议大部分代码应该在try/catch块之外,并且只检查重要的部分。这位微软员工补充说,即将发布的白皮书对错误的尝试/捕获块提出了警告。
我环顾四周,发现它会影响优化,但它似乎只适用于在作用域之间共享变量时。
我不是在问代码的可维护性,甚至不是在问如何处理正确的异常(毫无疑问,有问题的代码需要重构)。我也不是指使用异常进行流控制,这在大多数情况下显然是错误的。这些都是重要的问题(有些更重要),但不是这里的重点。
当不抛出异常时,try/catch块如何影响性能?
在看到有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. init)上创建了一个局部异常对象。例外ex)之后的事情与没有try/catch的方法几乎相同,直到第17行IL_0021: leave。IL_002f。如果发生异常,则控件跳转到IL_0025: ldloc行。0否则跳转到标签IL_002d: leave。s IL_002f和函数返回。
我可以安全地假设,如果没有异常发生,那么创建局部变量只保存异常对象和跳转指令的开销。
检查它。
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. init)上创建了一个局部异常对象。例外ex)之后的事情与没有try/catch的方法几乎相同,直到第17行IL_0021: leave。IL_002f。如果发生异常,则控件跳转到IL_0025: ldloc行。0否则跳转到标签IL_002d: leave。s IL_002f和函数返回。
我可以安全地假设,如果没有异常发生,那么创建局部变量只保存异常对象和跳转指令的开销。
在本m的例子中,结构是不同的。它将在内部的for循环中扩展开销,这将导致它不能很好地比较两种情况。
下面是更准确的比较,整个代码检查(包括变量声明)在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的原始测试代码时,我注意到Debug和Releas配置中的差异。
这个版本,我注意到在调试版本中有一个不同(实际上比其他版本更多),但在发布版本中没有任何不同。
结论:
基于这些测试,我认为我们可以说Try/Catch确实对性能有很小的影响。
编辑:
我尝试将循环值从10000000增加到1000000000,并在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的版本实际上更快!
我试了一个深接球。
static void TryCatch(int level, int max)
{
try
{
if (level < max) TryCatch(level + 1, max);
}
catch
{ }
}
static void NoTryCatch(int level, int max)
{
if (level < max) NoTryCatch(level + 1, max);
}
static void Main(string[] args)
{
var s = new Stopwatch();
const int max = 10000;
s.Start();
TryCatch(0, max);
s.Stop();
Console.WriteLine("try-catch " + s.Elapsed);
s.Restart();
NoTryCatch(0, max);
s.Stop();
Console.WriteLine("no try-catch " + s.Elapsed);
}
结果:
try-catch 00:00:00.0008528
no try-catch 00:00:00.0002422