在一次微软员工的代码检查中,我们在一个try{}块中发现了一大块代码。她和一位IT代表表示,这可能会对代码的性能产生影响。事实上,他们建议大部分代码应该在try/catch块之外,并且只检查重要的部分。这位微软员工补充说,即将发布的白皮书对错误的尝试/捕获块提出了警告。

我环顾四周,发现它会影响优化,但它似乎只适用于在作用域之间共享变量时。

我不是在问代码的可维护性,甚至不是在问如何处理正确的异常(毫无疑问,有问题的代码需要重构)。我也不是指使用异常进行流控制,这在大多数情况下显然是错误的。这些都是重要的问题(有些更重要),但不是这里的重点。

当不抛出异常时,try/catch块如何影响性能?


. net异常模型的非常全面的解释。

Rico Mariani的性能花絮:异常成本:何时抛出,何时不抛出

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.

德米特里·扎斯拉夫斯基:

根据Chris Brumme的注释:有 还有一个与事实有关的成本 有些优化没有进行 由JIT在现场执行 抓


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


检查它。

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的实际影响,它本身太小,在任何正常情况下都不会成为性能问题。

如果循环只做了很少的工作(在我的测试中,我做了一个x++),您可以测量异常处理的影响。带有异常处理的循环的运行时间大约长了10倍。

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


try catch块对性能的影响可以忽略不计,但异常抛出可能相当大,这可能是你的同事感到困惑的地方。


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


try/catch对性能有影响。

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

这里有一个关于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/catch实现的讨论,了解try/catch块如何工作,以及一些实现的开销如何高,而一些实现的开销为零。 当没有异常发生时。特别是,我认为Windows 32位实现有很高的开销,而64位实现没有。


理论上,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。如果thing1是昂贵的,这可能是一个主要的优化,尽管thing1是昂贵的情况也是编译器最不可能优化的。假设代码被更改:

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

Now there exists a sequence of events where statement3 could execute without statement2 having executed. Even if nothing in the code for thing2 could throw an exception, it would be possible that another thread could use an Interlocked.CompareExchange to notice that q was cleared and set it to Thread.ResetAbort, and then perform a Thread.Abort() before statement2 wrote its value to x. Then the catch would execute Thread.ResetAbort() [via delegate q], allowing execution to continue with statement3. Such a sequence of events would of course be exceptionally improbable, but a compiler is required to generate code which work according to specification even when such improbable events occur.

一般来说,编译器更有可能注意到遗漏简单代码而不是复杂代码的机会,因此如果从未抛出异常,try/catch很少会对性能产生很大影响。尽管如此,在某些情况下,try/catch块的存在可能会阻止优化——如果没有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

是的,尝试/捕捉会“损害”性能(一切都是相对的)。就浪费的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>();
        }
    }
}

我试了一个深接球。

        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