关于C#:try catch性能

try catch performance

这篇关于msdn的文章指出,您可以使用任意多的try-catch块,只要不引发实际的异常,就不会产生任何性能成本。因为我一直认为,即使不抛出异常,一个try-catch也会受到一个小的性能影响,所以我做了一个小测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 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));
        }

我得到的输出:

1
2
With try catch: 68
Without try catch: 34

所以,使用no-try-catch块似乎更快?更奇怪的是,当我将for循环体中的计算替换为更复杂的计算时,比如:c += i * (2 * (int)Math.Floor((double)i));。这种差异远没有那么显著。

1
2
With try catch: 640
Without try catch: 655

我是在做错事还是有合理的解释?


JIT不会对"受保护的"/"Try"块执行优化,我想这取决于您在Try/Catch块中编写的代码,这将影响您的性能。


Try/Catch/Finally/Fault块本身在优化的发布程序集中基本上没有开销。虽然catch和finally块经常会添加额外的IL,但如果不抛出异常,则行为上几乎没有差异。而不是简单的复试,通常有一个休假到稍后的复试。

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

如果在Try/Catch块中没有任何行为,那么"离开RET"与"只剩下RET"的额外成本将占主导地位,并且明显存在可测量的差异。但是,在Try子句中存在某种行为的任何其他情况下,块本身的成本都将被完全否定。


请注意,我只提供单声道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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 ;
    }
}

拆卸这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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_006直接指向leave IL_0013。在我的例子中,我的最佳猜测是,当编译为机器代码时,leave是一个(相对)昂贵的跳跃——这可能是也可能不是,特别是在for循环中。也就是说,try catch没有固有的开销,但是跳过catch有成本,就像任何条件分支一样。


实际的计算量是如此之小,以至于精确的测量非常困难。在我看来,Try-Catch可能会给程序增加一点固定的额外时间。我冒昧地猜测,在不知道C中如何实现异常的情况下,这主要是对异常路径的初始化,也许只是对JIT的一个轻微负载。

对于任何实际使用,花费在计算上的时间将压倒花在处理try catch上的时间,因此,try catch的成本可以被视为接近零。


问题首先出现在测试代码中。您使用了秒表.elapsed.millises,它只显示经过时间的毫秒部分,使用totalms获取整个部分…

如果不引发异常,则差异最小

但真正的问题是"我需要检查异常还是让C处理异常抛出?"

显然…独自处理…尝试运行此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

无尝试捕获:8.92E-05

令人惊奇的是,只有10000个物体,除了200个例外。

更正:我在调试时运行代码,并将异常写入输出窗口。这些是发布的结果开销大大减少,但仍有7500%的改进。

带Try-Catch:0.0546915

单独检查:0.0007294

使用Try-Catch抛出自己的同一异常对象:0.0265229


对于这样的测试,仅仅34毫秒的差异就小于误差范围。

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

在进行这种基准测试时,我尝试在每段代码上循环至少20秒,最好是更长的时间,理想情况下是几个小时。


有关如何使用Try/Catch块的讨论,请参见Try/Catch实现的讨论,有些实现的开销很高,有些实现的开销为零,当没有异常发生时。