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

Kob*_*obi 261 c# performance try-catch

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

我环顾四周,发现它可以影响优化,但它似乎只适用于范围之间共享变量.

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

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

Ben*_*n M 195

核实.

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);
}
Run Code Online (Sandbox Code Playgroud)

输出:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.
Run Code Online (Sandbox Code Playgroud)

以毫秒为单位:

449
416
Run Code Online (Sandbox Code Playgroud)

新代码:

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();
}
Run Code Online (Sandbox Code Playgroud)

新结果:

   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
Run Code Online (Sandbox Code Playgroud)

  • 这是一个调试版本.JIT没有对这些进行优化. (30认同)
  • 像这样的程序似乎不太适合测试异常处理的影响,在正常的try {} catch {}块中发生的事情将会被优化掉.我可能会出去吃午饭...... (27认同)
  • 你能否以相反的顺序尝试它们以确保JIT编译对前者没有影响? (24认同)
  • 这根本不是真的,想想看.你在循环中使用try catch多少次?大多数情况下,您将在try.c中使用循环 (7认同)
  • 真?"当不抛出异常时,try/catch块如何影响性能?" (7认同)
  • @BenM:优化不会"干扰"结果.优化的代码是我们感兴趣的唯一结果. (6认同)
  • 请注意,这似乎与问题的内容不同.这个问题表明,有些代码应该移到外面,只有一些代码用异常检查.换句话说,它不是完全摆脱try/catch,而是try/catch中的代码本身*比外部运行得慢*. (4认同)
  • @BenVoigt:OP 想知道 try/catch 块的性能影响*一般* - 显然,在此处启用优化会使结果作为该影响的一般度量毫无价值,因为优化的行为/好处是高度上下文-具体的。我已经怀疑这个测试作为一般衡量标准的价值,因为它是非常狭窄条件的单一基准;优化器会进一步专门化它,使其作为通用预测器的用处更小。 (3认同)
  • 实际上,如果可以的话,这也是问题的一部分.我们是否应该担心try/catch块,如果移动/移除它会有帮助(尽管很明显,它们很重要且不会被删除 - 你对这个jmgant是正确的).再一次,我的科学家对基准测试并不是很疯狂(除非大规模完成),所以我不会接受这个答案,但Ben确实得到了我的+1. (2认同)
  • @BenM:如果"这是一个调试版本",那么它对性能测试毫无用处.你甚至没有在答案本身中提到:( (2认同)
  • @BenVoigt:你误用了一个普遍合理的原则;在这种情况下,调试版本消除了优化干扰结果的任何可能性。 (2认同)
  • @BenVoigt:如果优化不会潜在地干扰(即更改)结果,那么您对调试版本的反对意见是什么?无论如何,避免优化的目的*在这种情况下*是从测试中删除一个变量;我们可以有理由相信,任何观察到的差异直接归因于 try/catch 语句的存在,而不是(例如)优化器采用的不同代码生成策略。 (2认同)
  • @BenM 好吧,您可以忽略它,但不幸的是,在这种情况下,这些测量值毫无用处。 (2认同)

The*_*iot 101

在看到try/catch和没有try/catch的所有统计数据后,好奇心迫使我向后看,看看两个案例的生成情况.这是代码:

C#:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

C#:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

我不是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和函数返回.

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

  • 好吧,IL包含一个与C#中相同的符号的try/catch块,所以这并没有真正显示try/catch在幕后意味着多少开销!只是IL没有添加更多,并不意味着它没有在编译的汇编代码中添加一些东西.IL只是所有.NET语言的通用表示.这不是机器代码! (32认同)
  • +1这是最好的答案.两者之间的差异非常明显(和温和). (3认同)

Joh*_*ica 62

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

  • 这是一个很好的观点 - 与我们列表中的其他项目相比,这个项目应该是微不足道的.我们应该相信基本语言功能才能正常运行,并优化我们可以控制的内容(sql,索引,算法). (9认同)
  • 想想紧密的循环配合。例如您从游戏服务器的套接字数据流中读取和反序列化对象的循环,并尝试尽可能多地挤压。因此,您使用 MessagePack 进行对象序列化而不是 binaryformatter,并使用 ArrayPool&lt;byte&gt; 而不是仅创建字节数组等... 一些优化将被编译器跳过,异常变量也会进入 Gen0 GC。我要说的是,在“某些”场景中,一切都会产生影响。 (5认同)

aru*_*rul 34

对.NET异常模型的全面解释.

Rico Mariani的表演花絮:例外成本:什么时候投掷,何时投注

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

Dmitriy Zaslavskiy:

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


awe*_*awe 25

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();
        }
Run Code Online (Sandbox Code Playgroud)

当我从Ben M运行原始测试代码时,我注意到Debug和Releas配置都有区别.

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

结论:
基于这些测试,我认为我们可以说的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
Run Code Online (Sandbox Code Playgroud)

你看到结果是不可能的.在某些情况下,使用Try/Catch的版本实际上更快!

  • 这不能证明您的机器同时正在执行各种其他任务吗?经过的时间永远不是一个好的衡量标准,您需要使用记录处理器时间而不是经过时间的分析器. (5认同)
  • 你在这里没有计时“try/catch”。您正在对 10M 循环进行 12 次 try/catch _entering-ritic-section_ 计时。循环的噪音将消除 try/catch 的任何影响。相反,如果您将 try/catch 放入紧密循环中,并与/不比较,您最终会得到 try/catch 的成本。(毫无疑问,这样的编码通常不是一个好的实践,但是如果您想计算构造的开销,那么您就是这样做的)。如今,BenchmarkDotNet 是可靠执行计时的首选工具。 (3认同)
  • 我也注意到了这一点,有时使用 try/catch 会更快。我已经对本的回答发表了评论。但是,与 24 名选民不同,我不喜欢这种基准测试,我认为这不是一个很好的指示。在这种情况下,代码会更快,但会一直如此吗? (2认同)
  • @Kobi:我认为这不是最好的基准测试方法,如果你要将它作为一个证明你的程序运行得比其他东西更快的证据,但可以给你一个开发人员,表明一种方法比另一种更好.在这种情况下,我认为我们可以说差异(至少对于Release配置)是可以忽略的. (2认同)

Guf*_*ffa 15

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

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

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


RHi*_*cke 11

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


小智 9

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

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}");
Run Code Online (Sandbox Code Playgroud)

结果如下:

With Checking:  20367
With Exception: 13998
Run Code Online (Sandbox Code Playgroud)


Isa*_*aac 8

try/catch HAS对性能的影响.

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

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

  • 复杂度是 O(1) 这并不意味着太多。例如,如果您使用 try-catch(或者您提到一个循环)配备一个经常被调用的代码部分,那么 O(1) 最终可以加起来为一个可测量的数字。 (4认同)

l33*_*33t 7

是的,try/catch会“损害”性能(一切都是相对的)。就浪费的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 |
Run Code Online (Sandbox Code Playgroud)

分析

一些附加说明。

|            Method | Code size | Inlineable |
|------------------ |---------- |-----------:|
|        NoTryCatch |        12 |        yes |
|      WithTryCatch |        18 |          ? |
| WithTryCatchThrow |        18 |         no |
Run Code Online (Sandbox Code Playgroud)

代码大小NoTryCatch()产生 12 个字节的代码,而 try/catch 添加另外 6 个字节。另外,每当写一篇文章时,try/catch您很可能会拥有一个或多个throw new Exception("Message", ex)语句,从而进一步“膨胀”代码。

这里最重要的是代码内联.NET关键字的存在意味着throw该方法永远不会被编译器内联(意味着代码速度较慢,但​​占用空间也更少)。我最近彻底测试了这个事实,所以它在.NET Core. 不确定是否try/catch遵循相同的规则。TODO: Verify!

完整的测试代码

|            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 |
Run Code Online (Sandbox Code Playgroud)


Ira*_*ter 5

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


sup*_*cat 5

理论上,除非实际发生异常,否则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
Run Code Online (Sandbox Code Playgroud)

编译器可能能够基于保证statement2在statement3之前执行的事实来优化statement1。如果编译器可以识别出thing1没有副作用并且thing2实际上没有使用x,则可以安全地完全省略thing1。如果thing1昂贵(如果是这种情况),那可能是一个主要的优化,尽管thing1昂贵的情况也是编译器最不可能优化的情况。假设代码已更改:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3
Run Code Online (Sandbox Code Playgroud)

现在存在一系列事件,其中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而言,这种优化会使代码运行得更快。