尝试捕获性能

27

这篇MSDN上的文章说:只要没有实际的异常被抛出,你可以使用任意多个try-catch块而不会产生任何性能损失。
因为我一直认为即使不抛出异常,使用try-catch也会带来轻微的性能损失,所以我进行了一个小测试。

 private void TryCatchPerformance()
        {
            int iterations = 100000000;

            Stopwatch stopwatch = Stopwatch.StartNew();
            int c = 0;
            for (int i = 0; i < iterations; i++)
            {
                try
                {
                   // c += i * (2 * (int)Math.Floor((double)i));
                    c += i * 2;
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
            stopwatch.Stop();
            WriteLog(String.Format("With try catch: {0}", stopwatch.ElapsedMilliseconds));

            Stopwatch stopwatch2 = Stopwatch.StartNew();
            int c2 = 0;
            for (int i = 0; i < iterations; i++)
            {
              //  c2 += i * (2 * (int)Math.Floor((double)i));
                c2 += i * 2;
            }
            stopwatch2.Stop();
            WriteLog(String.Format("Without try catch: {0}", stopwatch2.ElapsedMilliseconds));
        }
我得到的输出:
With try catch: 68
Without try catch: 34

所以似乎毫不使用 try-catch 块最终更快?

我觉得更奇怪的是,当我将 for 循环体中的计算替换为更复杂的内容时,例如:c += i * (2 * (int)Math.Floor((double)i));
差异就没有那么明显了。

With try catch: 640
Without try catch: 655

我在这里做错了什么,还是有一个逻辑上的解释?


1
你只是展示了基准测试比看起来更加困难。首先,你应该反转两个代码块并重新进行测量。 - H H
将这两个块反转并不会在输出中产生任何显著的差异。您能详细说明这为什么很重要吗? - Mez
2
有点晚了。但这是因为JIT可能已经做出了影响第二次运行的优化决策。 - Hector
7个回答

16

JIT 不会对 'protected' / 'try' 块进行优化,我猜想这取决于您在 try/catch 块中编写的代码,这将影响您的性能。


3
您可以参考此链接:http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx本文介绍了在编写代码时使用 try-catch-finally 块的性能影响。它强调了使用 try-catch-finally 可能会导致严重的性能问题,并提出了优化建议。因此,在编写代码时应该谨慎地使用 try-catch-finally 块来避免影响程序性能。 - P.K
5
这个回答是不正确的。优化并没有被禁用,只是在try块中可能不适用。添加try catch块并不会禁用优化。添加try块可能会改变代码的执行路径,这可能会影响JIT能够执行的优化。最大的问题似乎是跨越try块本身的变量,例如:比如int count = 0; try { ... count++; ... } finally { print(count); } - Robert Beuligmann

14

try/catch/finally/fault代码块在优化后的程序集中本身几乎没有额外开销。尽管在catch和finally块中通常会添加额外的IL代码,当没有抛出异常时,行为上也几乎没有差别。与简单的ret指令不同,通常有一个leave指令来返回。

try/catch/finally代码块的真正成本发生在处理异常时。在这种情况下,必须创建一个异常,放置堆栈跟踪标记,并且如果处理异常并访问其StackTrace属性,则会产生堆栈遍历。最重的操作是堆栈跟踪,它遵循先前设置的堆栈跟踪标记来构建一个StackTrace对象,该对象可用于显示错误发生的位置和通过的调用。

如果try/catch块中没有行为,则“leave to ret”和“ret”之间的额外成本将占主导地位,显然会有明显的差异。但是,在任何其他有一些行为的情况下,代码块本身的成本将完全被抵消。


6
请注意,我只有Mono可用:
// a.cs
public class x {
    static void Main() {
        int x = 0;
        x += 5;
        return ;
    }
}


// b.cs
public class x {
    static void Main() {
        int x = 0;
        try {
            x += 5;
        } catch (System.Exception) {
            throw;
        }
        return ;
    }
}

将它们分解开:
// a.cs
       default void Main ()  cil managed
{
    // Method begins at RVA 0x20f4
    .entrypoint
    // Code size 7 (0x7)
    .maxstack 3
    .locals init (
            int32   V_0)
    IL_0000:  ldc.i4.0
    IL_0001:  stloc.0
    IL_0002:  ldloc.0
    IL_0003:  ldc.i4.5
    IL_0004:  add
    IL_0005:  stloc.0
    IL_0006:  ret
} // end of method x::Main

并且

// b.cs
      default void Main ()  cil managed
{
    // Method begins at RVA 0x20f4
    .entrypoint
    // Code size 20 (0x14)
    .maxstack 3
    .locals init (
            int32   V_0)
    IL_0000:  ldc.i4.0
    IL_0001:  stloc.0
    .try { // 0
      IL_0002:  ldloc.0
      IL_0003:  ldc.i4.5
      IL_0004:  add
      IL_0005:  stloc.0
      IL_0006:  leave IL_0013

    } // end .try 0
    catch class [mscorlib]System.Exception { // 0
      IL_000b:  pop
      IL_000c:  rethrow
      IL_000e:  leave IL_0013

    } // end handler 0
    IL_0013:  ret
} // end of method x::Main

我看到的主要区别是a.cs在IL_0006处直接跳转到ret,而b.cs必须在IL_0013处通过leave IL_006进行跳转。我的最佳猜测是,在编译成机器代码时,leave是一个(相对)昂贵的跳转操作,这可能在你的for循环中并非如此。也就是说,try-catch没有固有的开销,但是跳过catch有一个成本,就像任何条件分支一样。

加1分是为了鼓励,但实际上for循环才是主要因素。 - H H

5
实际计算量非常小,因此准确测量非常棘手。在我看来,try-catch可能会为例程添加非常少量的额外时间。我猜测(不知道C#中如何实现异常处理),这主要是异常路径的初始化,可能只对JIT造成轻微的负载。
对于任何实际用途,计算所花费的时间将远远超过与try-catch的操作所花费的时间,因此可以将try-catch的成本视为接近于零。

2
问题首先出在你的测试代码上。 你使用了stopwatch.Elapsed.Milliseconds,它只显示流逝时间的毫秒部分, 使用TotalMilliseconds可以得到整个部分...
如果没有抛出异常,差异是微小的。
但真正的问题是“我需要检查异常还是让C#处理异常抛出?” 显然...单独处理... 尝试运行这个:
private void TryCatchPerformance()
    {
        int iterations = 10000;
        textBox1.Text = "";
        Stopwatch stopwatch = Stopwatch.StartNew();
        int c = 0;
        for (int i = 0; i < iterations; i++)
        {
            try
            {   
                c += i / (i % 50);
            }
            catch (Exception)
            {

            }
        }
        stopwatch.Stop();
        Debug.WriteLine(String.Format("With try catch: {0}", stopwatch.Elapsed.TotalSeconds));

        Stopwatch stopwatch2 = Stopwatch.StartNew();
        int c2 = 0;
        for (int i = 0; i < iterations; i++)
        {
            int iMod50 = (i%50);
            if(iMod50 > 0)
                c2 += i / iMod50;
        }
        stopwatch2.Stop();
        Debug.WriteLine( String.Format("Without try catch: {0}", stopwatch2.Elapsed.TotalSeconds));

    }

输出: 已过时:请看下面! 使用try catch: 1.9938401

不使用try catch: 8.92E-05

令人惊叹,只有10000个对象,却有200个异常。

更正: 我在DEBUG模式下运行我的代码,并将VS编写的异常输出到窗口中。 这些是RELEASE的结果 开销要小得多,但仍然提高了7500%。

使用try catch: 0.0546915

仅检查:0.0007294

使用try catch抛出相同的异常对象:0.0265229


1
鉴于他的测试显示出超过一毫秒的差异,显示微秒似乎并不是非常有用的。 - headsling

0

请参阅有关try/catch实现的讨论,了解try/catch块的工作原理,以及一些实现在没有异常发生时具有高开销,而另一些实现则具有零开销。


0

仅相差34毫秒的差异比这样的测试的误差范围还要小。

正如您所注意到的,当您增加测试持续时间时,这种差异就会消失,两组代码的性能实际上是相同的。

在进行此类基准测试时,我尝试循环执行每个代码部分至少20秒,最好更长,并且最好持续数小时。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接