关于例外:C#中’尝试’的性能成本

Performance Cost Of 'try' in C#

我知道异常会造成性能损失,而且尝试和避免异常通常比在所有事情上放弃一个大的尝试/捕获更有效——但是Try块本身呢?仅仅声明一个尝试/捕获的代价是什么,即使它从不抛出异常?


试用的性能成本很低。异常处理的主要成本是获取堆栈跟踪和其他元数据,而这是一个直到您实际必须抛出异常时才支付的成本。

但这将因语言和实现而异。为什么不用C语言写一个简单的循环,然后自己计时呢?


实际上,几个月前我正在创建一个ASP.NET Web应用程序,我不小心用一个很长的循环包装了一个try/catch块。尽管循环并没有生成所有异常,但完成它却花费了太多时间。当我返回并看到try/catch被循环包围时,我反过来做了,我将循环包围在try/catch块中。性能提高了很多。你可以自己试试这个:做一些像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int total;

DateTime startTime = DateTime.Now;

for(int i = 0; i < 20000; i++)
{
try
{
total += i;
}
catch
{
// nothing to catch;
}
}

Console.Write((DateTime.Now - startTime).ToString());

然后取出Try/Catch块。你会看到很大的不同!


一个常见的说法是,异常在被捕获时是昂贵的,而不是抛出。这是因为大多数异常元数据收集(例如获取堆栈跟踪等)实际上只发生在try-catch端(而不是throw端)。

展开堆栈实际上很快——clr会向调用堆栈走去,只注意找到的最后一个块;在纯try finally块中,运行时不会尝试"完成"异常(元数据等)。

根据我的记忆,任何带有过滤器的尝试捕获(例如"catch(fooexception)")都是一样昂贵的-即使它们不做任何异常。

我冒昧地说,有以下块的方法(称为catchesandrethrows):

1
2
3
4
5
6
7
8
try
{
    ThrowsAnException();
}
catch
{
    throw;
}

可能导致在方法中更快的堆栈遍历-例如:

1
2
3
4
5
6
7
8
try
{
    CatchesAndRethrows();
}
catch (Exception ex) // The runtime has already done most of the work.
{
    // Some fancy logic
}

一些数字:

1
2
3
With: 0.13905ms
Without: 0.096ms
Percent difference: 144%

以下是我运行的基准(记住,发布模式-不调试运行):

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    static void Main(string[] args)
    {
        Stopwatch withCatch = new Stopwatch();
        Stopwatch withoutCatch = new Stopwatch();

        int iterations = 20000;

        for (int i = 0; i < iterations; i++)
        {
            if (i % 100 == 0)
            {
                Console.Write("{0}%", 100 * i / iterations);
                Console.CursorLeft = 0;
                Console.CursorTop = 0;
            }

            CatchIt(withCatch, withoutCatch);
        }

        Console.WriteLine("With: {0}ms", ((float)(withCatch.ElapsedMilliseconds)) / iterations);
        Console.WriteLine("Without: {0}ms", ((float)(withoutCatch.ElapsedMilliseconds)) / iterations);
        Console.WriteLine("Percent difference: {0}%", 100 * withCatch.ElapsedMilliseconds / withoutCatch.ElapsedMilliseconds);
        Console.ReadKey(true);
    }

    static void CatchIt(Stopwatch withCatch, Stopwatch withoutCatch)
    {
        withCatch.Start();

        try
        {
            FinallyIt(withoutCatch);
        }
        catch
        {
        }

        withCatch.Stop();
    }

    static void FinallyIt(Stopwatch withoutCatch)
    {
        try
        {
            withoutCatch.Start();
            ThrowIt(withoutCatch);
        }
        finally
        {
            withoutCatch.Stop();
        }
    }

    private static void ThrowIt(Stopwatch withoutCatch)
    {
        throw new NotImplementedException();
    }


要了解它真正的成本,您可以运行下面的代码。它需要一个简单的二维数组并生成超出范围的随机坐标。如果您的异常只发生一次,当然您不会注意到它。我的示例是为了强调在进行几千次这样的操作时它意味着什么,以及捕获异常与实现简单测试相比将节省您的时间。

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
43
44
45
46
47
        const int size = 1000;
        const int maxSteps = 100000;

        var randomSeed = (int)(DateTime.UtcNow - new DateTime(1970,1,1,0,0,0).ToLocalTime()).TotalMilliseconds;
        var random = new Random(randomSeed);
        var numOutOfRange = 0;
        var grid = new int[size,size];
        var stopwatch = new Stopwatch();
        Console.WriteLine("---Start test with exception---");
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < maxSteps; i++)
        {
            int coord = random.Next(0, size * 2);
            try
            {
                grid[coord, coord] = 1;
            }
            catch (IndexOutOfRangeException)
            {
                numOutOfRange++;
            }
        }
        stopwatch.Stop();
        Console.WriteLine("Time used:" + stopwatch.ElapsedMilliseconds +"ms, Number out of range:" + numOutOfRange);
        Console.WriteLine("---End test with exception---");

        random = new Random(randomSeed);

        stopwatch.Reset();
        Console.WriteLine("---Start test without exception---");
        numOutOfRange = 0;
        stopwatch.Start();
        for (int i = 0; i < maxSteps; i++)
        {
            int coord = random.Next(0, size * 2);
            if (coord >= grid.GetLength(0) || coord >= grid.GetLength(1))
            {
                numOutOfRange++;
                continue;
            }
            grid[coord, coord] = 1;
        }
        stopwatch.Stop();
        Console.WriteLine("Time used:" + stopwatch.ElapsedMilliseconds +"ms, Number out of range:" + numOutOfRange);
        Console.WriteLine("---End test without exception---");
        Console.ReadLine();

此代码的示例输出:

1
2
3
4
5
6
---Start test with exception---
Time used: 3228ms, Number out of range: 49795
---End test with exception---
---Start test without exception---
Time used: 3ms, Number out of range: 49795
---End test without exception---

您可能希望了解结构化异常处理。它是Window对异常的实现,并在.NET中使用。

http://www.microsoft.com/msj/0197/exception/exception.aspx