关于C#:当不抛出异常时,try/catch块会损害性能吗?

Do try/catch blocks hurt performance when exceptions are not thrown?

在与一位微软员工进行代码审查时,我们在一个try{}块中发现了一大块代码。她和一位IT代表建议,这可能会影响代码的性能。实际上,他们建议大多数代码应该在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
32
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);
}

输出:

1
2
00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

以毫秒为单位:

1
2
449
416

新代码:

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
33
34
35
36
37
38
39
40
41
42
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();
}

新结果:

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


在看到了所有关于try/catch和without try/catch的统计数据之后,好奇心迫使我向后看,看看这两种情况都产生了什么。代码如下:

C:

1
2
3
private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1));
}

MSIL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.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:

1
2
3
4
5
6
7
8
private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1));
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

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
.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)上创建了一个局部异常对象,之后,在第十七行IL_0021: leave.s IL_002f之前,这些对象与不使用try/catch的方法非常相同。如果发生异常,控件跳到行IL_0025: ldloc.0,否则我们跳到标签IL_002d: leave.s IL_002f并返回函数。

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


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


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

里科·马里亚尼的表演小道消息:例外成本:什么时候扔,什么时候不扔

The first kind of cost is the static
cost of having exception handling in
your code at all. Managed exceptions
actually do comparatively well here,
by which I mean the static cost can be
much lower than say in C++. Why is
this? Well, static cost is really
incurred in two kinds of places:
First, the actual sites of
try/finally/catch/throw where there's
code for those constructs. Second, in
unmanged code, there's the stealth
cost associated with keeping track of
all the objects that must be
destructed in the event that an
exception is thrown. There's a
considerable amount of cleanup logic
that must be present and the sneaky
part is that even code that doesn't
itself throw or catch or otherwise
have any overt use of exceptions still
bears the burden of knowing how to
clean up after itself.

德米特里·扎斯拉夫斯基:

As per Chris Brumme's note: There is
also a cost related to the fact the
some optimization are not being
performed by JIT in the presence of
catch


这个例子中的结构与ben m不同,它将在内部for循环中扩展开销,这将导致两种情况的比较不好。

当要检查的整个代码(包括变量声明)位于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
32
        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配置都有不同。

在这个版本中,我注意到了调试版本的不同(实际上比其他版本要多),但在发布版本中没有区别。

结论:基于这些测试,我认为我们可以说try/catch对性能的影响很小。

编辑:我尝试将循环值从10000000增加到100000000,然后在版本中再次运行以获得版本中的一些差异,结果是:

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
   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的版本实际上更快!


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

如果循环几乎不起作用(在我的测试中,我做了一个x++),您可以测量异常处理的影响。带有异常处理的循环的运行时间约为10倍。

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


Try-Catch块对性能的影响微乎其微,但异常抛出可能相当大,这可能是您的同事感到困惑的地方。


尝试/捕获对性能有影响。

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

这里有一个关于Try/Catch性能的参考(虽然没有解释它的复杂性,但它是隐含的)。看看抛出更少的异常部分


理论上,除非实际发生异常,否则try/catch块不会影响代码行为。然而,在一些罕见的情况下,尝试/捕获块的存在可能会产生重大影响,而在一些不常见但几乎不模糊的情况下,效果可能会明显。原因是给定的代码如下:

1
2
3
4
5
6
7
8
9
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

编译器可以根据保证语句2在语句3之前执行的事实来优化语句1。如果编译器能够识别出thing1没有副作用,thing2实际上没有使用x,那么它可以安全地完全忽略thing1。如果[在本例中]thing1很昂贵,这可能是一个主要的优化,尽管thing1很昂贵的情况也是编译器最不可能优化的情况。假设代码已更改:

1
2
3
4
5
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将通过委托q执行Thread.ResetAbort(),允许执行继续进行陈述3。当然,这样的事件序列是非常不可能的,但是编译器需要生成代码,即使在这种不可能的事件发生时,代码也可以根据规范工作。

一般来说,编译器更容易注意到遗漏简单代码位的机会,而不是遗漏复杂代码位的机会,因此,如果不抛出异常,则很少会出现try/catch会影响性能的情况。尽管如此,在某些情况下,存在try/catch块可能会阻止优化(但对于try/catch而言),从而使代码运行更快。


请参阅关于Try/Catch实现的讨论,以了解Try/Catch块如何工作,以及某些实现的开销如何很大,而有些实现的开销为零,当没有异常发生时。特别是,我认为Windows32位实现有很高的开销,而64位实现没有。