在一次微软员工的代码检查中,我们在一个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和函数返回。

我可以安全地假设,如果没有异常发生,那么创建局部变量只保存异常对象和跳转指令的开销。

其他回答

不。如果try/finally块排除的琐碎优化实际上对您的程序有可衡量的影响,那么您可能一开始就不应该使用. net。

请参阅try/catch实现的讨论,了解try/catch块如何工作,以及一些实现的开销如何高,而一些实现的开销为零。 当没有异常发生时。特别是,我认为Windows 32位实现有很高的开销,而64位实现没有。

是的,尝试/捕捉会“损害”性能(一切都是相对的)。就浪费的CPU周期而言,这并不多,但还有其他重要的方面需要考虑:

代码大小 方法内联

基准

首先,让我们使用一些复杂的工具(例如BenchmarkDotNet)检查速度。编译为Release (AnyCPU),在x64机器上运行。我想说没有区别,即使测试确实会告诉我们NoTryCatch()是一个很小很小的一点快:

|            Method |   N |     Mean |     Error |    StdDev |
|------------------ |---- |---------:|----------:|----------:|
|        NoTryCatch | 0.5 | 3.770 ns | 0.0492 ns | 0.0411 ns |
|      WithTryCatch | 0.5 | 4.060 ns | 0.0410 ns | 0.0384 ns |
| WithTryCatchThrow | 0.5 | 3.924 ns | 0.0994 ns | 0.0881 ns |

分析

一些额外的注释。

|            Method | Code size | Inlineable |
|------------------ |---------- |-----------:|
|        NoTryCatch |        12 |        yes |
|      WithTryCatch |        18 |          ? |
| WithTryCatchThrow |        18 |         no |

代码大小NoTryCatch()在代码中产生12个字节,而try/catch则增加6个字节。此外,每当编写try/catch语句时,您很可能会有一个或多个throw new Exception(“Message”,ex)语句,进一步“膨胀”代码。

这里最重要的是代码内联。在. net中,仅仅throw关键字的存在就意味着该方法永远不会被编译器内联(意味着代码更慢,但占用空间更少)。我最近彻底测试了这个事实,所以它在。net Core中似乎仍然有效。不确定try/catch是否遵循相同的规则。待办事项:验证!

完整的测试代码

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace TryCatchPerformance
{
    public class TryCatch
    {
        [Params(0.5)]
        public double N { get; set; }

        [Benchmark]
        public void NoTryCatch() => Math.Sin(N);

        [Benchmark]
        public void WithTryCatch()
        {
            try
            {
                Math.Sin(N);
            }
            catch
            {
            }
        }

        [Benchmark]
        public void WithTryCatchThrow()
        {
            try
            {
                Math.Sin(N);
            }
            catch (Exception ex)
            {
                throw;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<TryCatch>();
        }
    }
}

在本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的版本实际上更快!

虽然“预防胜于处理”,但从性能和效率的角度来看,我们可以选择试接而不是预变。考虑下面的代码:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

结果如下:

With Checking:  20367
With Exception: 13998