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


274

在与Microsoft员工进行代码审查期间,我们在一个try{}块内遇到了很大一部分代码。她和一位IT代表建议,这可能会影响代码的性能。实际上,他们建议大多数代码应位于try / catch块之外,并且仅应检查重要部分。微软员工补充说,即将发布的白皮书警告不要尝试错误的try / catch块。

我环顾四周,发现它会影响优化,但似乎仅在范围之间共享变量时才适用。

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

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


147
“为表现牺牲正确性的人都不应该得到。”
Joel Coehoorn,

16
就是说,不一定总是为了性能而牺牲正确性。
丹·戴维斯·布拉克特

19
简单的好奇心如何?
萨曼莎·布兰纳姆

63
@Joel:也许Kobi出于好奇想知道答案。知道性能会好还是坏并不一定意味着他会为自己的代码做任何疯狂的事情。为自己的利益而追求知识不是一件好事吗?
路加福音

6
这是一个知道是否进行此更改的好算法。首先,设定有意义的基于客户的绩效目标。其次,首先编写正确且清晰的代码。第三,对照您的目标进行测试。第四,如果您实现了目标,请早点工作,然后去海滩。第五,如果您没有达到目标,请使用探查器查找太慢的代码。第六,如果由于不必要的异常处理程序而导致该代码太慢,则仅删除异常处理程序。如果不是,请修复实际上太慢的代码。然后返回到第三步。
Eric Lippert

Answers:


203

核实。

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

24
您是否也可以按相反的顺序尝试它们,以确保JIT编译对前者没有影响?
2009年

28
像这样的程序似乎很难测试异常处理的影响,因为正常try {} catch {}块中发生的太多事情都将被优化。我可能会在那吃午饭……
LorenVS

30
这是一个调试版本。JIT不会优化这些。
本M

7
完全不是真的,请考虑一下。您使用多少次尝试循环捕获?大多数时候,您会在try.c中使用循环
-Athiwat Chunlakhan

9
真?“不抛出异常时,try / catch块如何影响性能?”
本M

105

看到所有的统计信息与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),这与在17行之前没有try / catch的方法非常相似IL_0021: leave.s IL_002f。如果发生异常,则控件跳转到行,IL_0025: ldloc.0否则我们跳转到标签IL_002d: leave.s IL_002f,函数返回。

我可以放心地假设,如果没有异常发生,那么创建局部变量包含异常对象和跳转指令就是开销。


33
好吧,IL包含与C#中相同的表示法的try / catch块,因此,这实际上并未显示try / catch在幕后意味着多少开销!只是IL不增加太多,并不意味着它与未在编译的汇编代码中添加任何东西一样。IL只是所有.NET语言的通用表示形式。这不是机器代码!
敬畏

64

否。如果try / finally块所进行的微不足道的优化实际上对您的程序产生了可衡量的影响,那么您可能一开始就不应该使用.NET。


10
这是一个很好的观点-与我们列表中的其他项目相比,该项目应该微不足道。我们应该相信基本的语言功能能够正确运行,并优化我们可以控制的内容(SQL,索引,算法)。
Kobi

3
想想紧密循环的伴侣。以循环为例,在该循环中,您从游戏服务器中的套接字数据流中读取对象并对其进行反序列化,并尝试尽可能地进行挤压。因此,您可以使用MessagePack进行对象序列化而不是binaryformatter,并使用ArrayPool <byte>而不是仅创建字节数组等。在这些情况下,紧密循环中多个(也许嵌套)try catch块的影响是什么。某些优化将被编译器跳过,并且异常变量将进入Gen0 GC。我要说的是,在“某些”场景中,一切都会产生影响。
tcwicks

35

.NET异常模型的详尽解释。

里科·马里亚尼(Rico Mariani)的表现花絮:异常费用:什么时候扔,什么时候不扔

第一种成本是在代码中完全具有异常处理的静态成本。托管异常实际上在这里表现相对较好,这意味着静态开销可能比C ++中的说法要低得多。为什么是这样?好吧,静态成本实际上是在两种情况下产生的:首先,在try / finally / catch / throw的实际站点中有这些构造的代码。其次,在未管理的代码中,与跟踪在引发异常时必须销毁的所有对象有关的隐性成本。必须存在大量的清理逻辑,而偷偷摸摸的部分是,即使代码没有

德米特里(Dmitriy Zaslavskiy):

根据Chris Brumme的说明:还有一个成本与JIT在存在catch的情况下未执行某些优化这一事实有关


1
关于C ++的问题是,标准库中很大一部分都会引发异常。他们没有任何选择。您必须使用某种异常策略来设计对象,一旦完成,便没有更多的隐形成本。
David Thornley,2009年

Rico Mariani的主张对于本机C ++是完全错误的。“静态成本可能比C ++中的成本低得多”-事实并非如此。虽然,我不确定在撰写本文时2003年的异常机制设计是什么。当抛出异常时,无论您有多少try / catch块以及它们在哪里,C ++ 都完全没有代价
BJovke '17

1
@BJovke C ++“零成本异常处理”仅意味着不抛出异常时没有运行时成本,但是由于所有清除代码都对异常调用析构函数,因此仍然存在较大的代码大小成本。同样,尽管在正常代码路径上没有生成任何特定于异常的代码,但代价实际上仍然不是零,因为异常的可能性仍然限制了优化器(例如,在需要保留异常的情况下需要保留的东西)周围某处->可以不太积极地丢弃值->效率较低的寄存器分配)
丹尼尔(Daniel

24

示例中的结构与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配置都不同。

我注意到这个版本在调试版本上有所不同(实际上比其他版本更多),但是在发行版中却没有什么不同。

结论
根据这些测试,我认为可以说“尝试/捕获”确实对性能有很小的影响。

编辑:
我试图将循环值从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

您会看到结果不一致。在某些情况下,使用“尝试/捕获”的版本实际上更快!


1
我也注意到了这一点,有时使用try / catch可以更快。我已经对Ben的回答发表了评论。但是,与24个投票者不同,我不喜欢这种基准测试,我认为这不是一个很好的指示。在这种情况下,代码会更快,但是会一直这样吗?
Kobi

5
这不是证明您的计算机同时在执行其他各种任务吗?经过的时间绝不是一个好方法,您需要使用记录处理器时间而不是经过的时间的探查器。
科林·戴斯蒙德2009年

2
@Kobi:我同意这不是基准测试的最佳方法,如果您要发布它来证明您的程序比其他程序运行得更快,但是可以作为开发人员向您表明一种方法的性能要优于另一种方法。在这种情况下,我认为可以说差异(至少对于Release配置而言)是可忽略的。
敬畏

1
您不在try/catch这里计时。您正在针对10M循环计时12次try / catch 进入关键部分。循环的噪声将消除尝试/捕获所产生的任何影响。相反,如果您将try / catch放入紧密循环中,然后与/不进行比较,则最终会得到try / catch的成本。(毫无疑问,这样的编码通常不是一个好习惯,但是如果您想计时构造的开销,那就是您的做法)。如今,BenchmarkDotNet是可靠执行时间的首选工具。
亚伯

15

try..catch在一个紧密的循环中测试了a的实际影响,它本身很小,在任何正常情况下都不会对性能造成影响。

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

如果循环确实完成了一些工作(在我的测试中,我称为Int32.Parse方法),则异常处理的影响很小,无法测量。通过交换循环的顺序,我得到了更大的不同...


11

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


8

尝试/捕获HAS对性能的影响。

但是影响不大。尝试/捕获复杂度通常为O(1),就像简单的赋值一样,除非将它们置于循环中。因此,您必须明智地使用它们。

这里是有关try / catch性能的参考(尽管没有解释它的复杂性,但是它暗含了)。看看“ 更少的异常抛出”部分


3
复杂度为O(1),并不意味着太多。例如,如果为一个代码段配备了try-catch(或提到一个循环),该代码段经常被调用,则O(1)最终可能会累加一个可测量的数字。
Csaba Toth

6

理论上,除非实际发生异常,否则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。如果编译器可以识别出事物1没有副作用,并且事物2实际上没有使用x,则它可以安全地完全省略事物1。如果thing1昂贵(如果是这种情况),那可能是一个重大的优化,尽管thing1昂贵的情况也是那些编译器最不可能优化的情况。假设代码已更改:

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

现在存在一系列事件,其中statement3可以在不执行statement2的情况下执行。即使for的代码中没有任何东西thing2可以引发异常,也有可能另一个线程可以使用an Interlocked.CompareExchange来注意到q已清除并将其设置为Thread.ResetAbort,然后执行Thread.Abort()before statement2将其值写入x。然后,catch将执行Thread.ResetAbort()[通过委托q],使执行继续执行statement3。这样的事件序列当然是极不可能发生的,但是即使发生了这种不太可能发生的事件,也需要编译器来生成根据规范工作的代码。

通常,与复杂的代码相比,编译器更有可能忽略掉一些简单的代码,因此,如果从未抛出异常,try / catch很少会影响性能。尽管如此,在某些情况下,try / catch块的存在可能会阻止优化,但对于try / catch而言,这种优化会使代码运行得更快。


5

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

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

4

请参见有关try / catch实现的讨论,以获取有关try / catch块如何工作的讨论,以及在无异常发生时某些实现的开销高,另一些开销为零的讨论。特别是,我认为Windows 32位实现的开销较高,而64位实现的则没有。


我所描述的是两种实现异常的不同方法。这些方法同样适用于C ++和C#以及托管/非托管代码。我不知道MS为C#选择了哪些,但是MS提供的机器级应用程序的异常处理体系结构使用了更快的方案。如果64位的C#实现没有使用它,我会感到有些惊讶。
Ira Baxter
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.