首页 文章

当没有抛出异常时,try / catch块是否会损害性能?

提问于
浏览
219

在与Microsoft员工进行代码审查期间,我们在 try{} 块中遇到了大量代码 . 她和IT代表建议这可能会影响代码的性能 . 事实上,他们建议大多数代码应该在try / catch块之外,并且只应该检查重要的部分 . 微软员工补充说,即将发布的白皮书警告不要使用不正确的try / catch块 .

我环顾四周,发现它can affect optimizations,但它似乎只适用于范围之间共享变量 .

我不是在询问代码的可维护性,甚至不是在处理正确的异常(有问题的代码需要重新分解,毫无疑问) . 我也没有提到使用流量控制的异常,这在大多数情况下显然是错误的 . 这些都是重要的问题(有些更重要),但不是重点 .

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

10 回答

  • 51

    try / catch HAS对性能的影响 .

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

    Here是关于try / catch性能的参考(虽然没有解释它的复杂性,但它暗示了) . 看一下 Throw Fewer Exceptions 部分

  • 3

    核实 .

    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
    
  • 21

    在看到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) 上创建了一个本地异常对象,之后事情与没有try / catch的方法完全相同,直到第十七行 IL_0021: leave.s IL_002f . 如果发生异常,控件将跳转到第 IL_0025: ldloc.0 行,否则我们跳转到标签 IL_002d: leave.s IL_002f 并返回函数 .

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

  • 84

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

  • 10

    Quite comprehensive explanation of the .NET exception model.

    Rico Mariani的表演花絮:Exception Cost: When to throw and when not to

    第一种成本是在代码中进行异常处理的静态成本 . 托管异常实际上在这里做得比较好,我的意思是静态成本可以比C中的低得多 . 为什么是这样?好吧,静态成本实际上是在两种情况下产生的:首先,try / finally / catch / throw的实际站点,其中有这些构造的代码 . 其次,在无人值守的代码中,存在与跟踪在抛出异常时必须被破坏的所有对象相关联的隐形成本 . 必须存在相当数量的清理逻辑,并且偷偷摸摸的部分是即使代码本身不会抛出或捕获或以其他方式明显使用异常仍然承担着知道如何清理自身的负担 .

    Dmitriy Zaslavskiy:

    根据Chris Brumme的说明:还有一个成本与JIT在捕获存在时没有执行某些优化有关

  • 6

    该示例中的结构与 Ben 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配置都有区别 .

    这个版本,我注意到调试版本的差异(实际上比其他版本更多),但它在Release版本中没有区别 .

    Conclution
    根据这些测试,我认为我们可以说Try / Catch does 对性能的影响很小 .

    EDIT:
    我试图将循环值从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的版本实际上更快!

  • 165

    我在紧密循环中测试了 try..catch 的实际影响,并且在任何正常情况下它本身都太小而不是性能问题 .

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

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

  • 4

    尝试catch块对性能的影响可以忽略但是异常投掷可能相当大,这可能是你的同事感到困惑的地方 .

  • 13

    理论上,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
    

    现在存在一系列事件,其中statement3可以在没有执行statement2的情况下执行 . 即使 thing2 的代码中没有任何内容可以抛出异常,也可能是另一个线程可以使用 Interlocked.CompareExchange 来注意 q 被清除并将其设置为 Thread.ResetAbort ,然后在statement2将其值写入 x 之前执行 Thread.Abort() . 然后 catch 将执行 Thread.ResetAbort() [通过委托 q ],允许继续执行statement3 . 这样的事件序列当然是特别不可能的,但是编译器需要生成根据规范工作的代码,即使这些不可能的事件发生也是如此 .

    一般来说,编译器更有可能注意到遗漏简单代码的机会而不是复杂代码,因此如果永远不会抛出异常,try / catch很少会影响性能 . 尽管如此,在某些情况下,try / catch块的存在可能会阻止优化 - 但是对于try / catch - 会允许代码更快地运行 .

  • 31

    有关try / catch块如何工作的讨论,请参阅discussion on try/catch implementation,以及一些实现如何具有高开销,并且一些实现没有异常时的开销为零 . 特别是,我认为Windows 32位实现具有高开销,而64位实现则没有 .

相关问题