代码之家  ›  专栏  ›  技术社区  ›  Drevak

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

  •  7
  • Drevak  · 技术社区  · 15 年前

    在阅读了一个关于我们的计算机一秒钟内能做什么的问题之后,我做了一个小测试,我想了一会儿,结果让我非常惊讶。看:

    简单的程序捕获空异常,几乎需要一秒钟的时间来执行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次迭代(始终是大约的)

    注释 我用一个蹩脚的3.06GHz的PIV运行这个

    7 回复  |  直到 15 年前
        1
  •  12
  •   Jon Skeet    15 年前

    除非在调试程序中运行,否则1900次迭代都不会花费一秒钟的时间。在调试器下运行性能测试是一个坏主意。

    编辑:请注意,这不是更改为发布的情况 建造 -这是一个不使用调试器运行的情况;即,点击ctrl-f5而不是f5 .

    尽管如此,当你可以很容易地避免它们时,挑起例外是 一个坏主意。

    我对异常的性能的看法是:如果您恰当地使用它们,它们不应该导致重大的性能问题。 除非 无论如何,您正处于灾难性的情况下(例如,您尝试进行数十万次Web服务调用,但网络已关闭)。

    在调试程序下,异常是昂贵的——当然在Visual Studio中是如此——因为要确定是否要闯入调试程序等,并且可能要做一些不必要的堆栈分析。他们仍然 有点 不管怎样都很贵,但你不应该把它们扔得太多而引起注意。还有堆栈展开、相关的捕获处理程序查找等工作要做,但这应该只在发生问题时发生。

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

    循环体的非异常版本

    • 将空值赋给变量
    • 检查变量是否为空;它为空,因此返回循环的顶部

    (正如评论中提到的,JIT很可能会优化这一点…)

    异常版本:

    • 将空值赋给变量
    • 解引用变量
      • 无效隐式检查
      • 创建异常对象
      • 检查是否有任何要调用的筛选异常处理程序
      • 在堆栈中查找要跳转到的catch块
      • 检查任何finally块
      • 适当分支

    你看到的表演越来越少了,这有什么奇怪的吗?

    现在,将它与更常见的情况进行比较,在这种情况下,您要做大量的工作,可能是IO、对象创建等。 也许吧 引发异常。然后差异就变得不那么显著了。

        2
  •  2
  •   Gonzalo    15 年前

    退房 Chris Brumme's blog 特别注意 性能和趋势 有关异常为何变慢的解释。它们之所以被称为“例外”,是有原因的:它们不应该经常发生。

        3
  •  2
  •   Community T.Woody    7 年前

    您可能还会发现这个常见问题很有帮助: How slow are .NET exceptions?

        4
  •  2
  •   Community T.Woody    7 年前

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

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

        5
  •  1
  •   Raymond Tay    15 年前

    编译器执行的一种优化,我认为这可能是“死代码消除”;还取决于您使用的编译器,后一个程序实际上正在执行汇编程序民间所说的“无操作”。

        6
  •  1
  •   mYsZa    15 年前

    在我的测试中,“异常”代码没有那么慢——慢得多,但没有那么慢。 差异在于创建异常(或者,具体来说,是nullReferenceException)对象。其中最慢的部分是检索异常消息的字符串(有对getResourceString的内部调用)并获取堆栈跟踪。

        7
  •  0
  •   ShuggyCoUk    15 年前

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

    后一个“优化”循环具有编译时不变的特性,即测试始终为空,因此甚至不需要在尝试的赋值中进行编译。实际上,您正在测试一个空循环,每次都会引发一个异常。

    一个真正好的JIT甚至可以完全删除循环,注意循环没有主体,因此除了增加计数器之外没有副作用,计数器本身也没有使用(这是不太可能的,因为这样的优化在现实世界中几乎没有实用性)。

    抛出异常相当昂贵(相对于传统的分支控制流)[1]主要是由于以下三个原因:

    1. 所有异常都是引用类型,因此(目前)是堆分配的,随后是垃圾收集的。
    2. 填充到异常中的堆栈级别(这与堆栈展开的距离成比例-您的示例无法完全测量的距离)
    3. 进入异常处理代码可以跳过所有的好事情,比如分支预测,这使得今天的深入流水线处理程序能够让自己做一些有用的事情。

    不管怎样,在一个紧密的循环中抛出和捕获异常几乎肯定是一个有很大缺陷的设计,但是如果您试图测量这种影响,那么您应该编写一个实际做到这一点的循环。


    1. 这里很贵 非常 相对项。您仍然可以在普通硬件上每秒执行数万次。