为什么这种性能差异? (异常捕获)

时间:2009-12-07 14:05:14

标签: c# performance exception-handling

在这里读了一个关于我们的计算机可以在一秒钟内完成什么事情的问题后,我做了一个小测试,我想了一会儿,我对结果感到非常惊讶。看:

捕获空异常的简单程序,需要将近一秒钟来完成1900次迭代:

for(long c = 0; c < 200000000; c++)
{
    try
    {
        test = null;
        test.x = 1;
    }
    catch (Exception ex)
    {
    }
}

或者,在进行赋值之前检查test == null,相同的pogram可以在一秒钟内完成200000000次迭代。

for(long c = 0; c < 1900; c++)
{
    test = null;
    f (!(test == null))
    {
        test.x = 1;
    }
}

任何人都有详细解释为什么会出现这种巨大的差异?

编辑:在发布模式下运行测试,在Visual Studio之外我得到35000-40000次迭代与400000000次迭代(总是aprox)

注意我用蹩脚的PIV 3.06Ghz运行这个

7 个答案:

答案 0 :(得分:12)

除非你在调试器中运行,否则没有办法在1900次迭代中花费一秒钟。在调试器下运行性能测试是一个坏主意。

编辑:请注意,这不是更改为发布 build 的情况 - 这是在没有调试器的情况下运行的情况;即按Ctrl-F5而不是F5

话虽如此,当你可以很容易地避免它们时引发异常是一个坏主意。

我对异常表现的看法:如果您正确使用它们,它们不应该导致重大的性能问题除非您处于某种灾难性的情况(例如,您正在尝试制作成千上万的网络服务电话并且网络已关闭。

异常在调试器下很昂贵 - 当然在Visual Studio中,无论如何 - 由于计算是否要进入调试器等,并且可能进行任何数量的堆栈分析,否则这是不必要的。无论如何,它们仍然有点昂贵,但你不应该投入足够的注意力。仍然有堆栈展开,相关的捕获处理程序等等 - 但这应该只在首先出现问题时才会发生。

编辑:当然,抛出一个异常仍然会让你每秒的迭代次数减少(尽管35000仍然是一个非常低的数字 - 我期望超过100K)因为你几乎没有没有在非例外情​​况下。让我们看看这两个:

循环体的非异常版本

  • 将null指定给变量
  • 检查变量是否为空;它是,所以回到循环的顶部

(正如评论中提到的,JIT很可能无论如何都会优化它......)

例外版本:

  • 将null指定给变量
  • 取消引用变量
    • 隐式检查无效
    • 创建例外对象
    • 检查要调用的任何已过滤的异常处理程序
    • 查找堆栈以获取catch块以跳转到
    • 检查任何finally块
    • 适当分支

难道你看到的表现较差吗?

现在将其与您执行大量工作(可能是IO,对象创建等)的更常见情况进行比较 - 并且可能抛出异常。然后差异变得不那么显着了。

答案 1 :(得分:2)

请查看Chris Brumme's blog,特别注意效果和趋势部分,了解异常缓慢的原因。它们被称为“例外”是有原因的:它们不应经常发生。

答案 2 :(得分:2)

您可能还会发现这个热门问题很有用:How slow are .NET exceptions?

答案 3 :(得分:2)

这里有另一个因素。如果在执行目录中有.pdb文件,那么当抛出异常时,.NET运行时将读取.pdb文件以获取要包含在异常堆栈跟踪中的代码行号。这需要相当多的时间。尝试在执行目录中使用和不使用.pdb文件的第一种方法(带例外的方法)。

我做了一个简单的计时测试,有或没有.pdb作为另一个问题here的答案。

答案 4 :(得分:1)

编译器执行的优化,我相信它可能是“死代码消除”;也取决于您正在使用的后一个程序编译器实际上正在做汇编程序民间称之为“无操作”。

答案 5 :(得分:1)

在我的测试中,“特殊”代码并不那么慢 - 慢得多,但不是那么多。 差异在于创建Exception(或者,具体地说,NullReferenceException)对象。其中最慢的部分是检索异常消息的字符串 - 内部调用GetResourceString - 并获取堆栈跟踪。

答案 6 :(得分:0)

这是一个糟糕的微观基准。

后一个'优化'循环具有编译时不变性,测试始终为空,因此在尝试的赋值中甚至不需要编译。您实际上每次都抛出异常来测试一个空循环。

一个非常好的jit甚至可以完全删除循环,注意循环没有主体,因此除了递增计数器之外没有副作用,并且计数器本身未被使用(这不太可能,因为这样的优化会有现实世界中的小实用工具)。

抛出相对于常规分支控制流程的例外费用相当昂贵[1]主要由于以下三点:

  1. 所有异常都是引用类型,因此(现在)是堆分配并随后进行垃圾回收。
  2. 填充异常的堆栈级别(这与堆栈展开的距离成正比 - 您的示例无法完全测量)
  3. 进入异常处理代码会跳过分支预测这样的好东西,让今天的流水线处理器让自己做一些有用的事情
  4. 在紧密循环中抛出和捕捉异常几乎肯定是一个非常有缺陷的设计,但如果你试图衡量这种影响,你应该写一个实际上就是这样做的循环。


    1. 这里的昂贵是一个非常的相对术语。在适度的硬件上,您仍然可以每秒执行数万个它们。