【问题标题】:Why this performance difference? (Exception catching)为什么会有这种性能差异? (异常捕捉)
【发布时间】:2010-12-24 00:41:35
【问题描述】:

在这里阅读了一个关于我们的计算机可以在一秒钟内做什么的问题后,我做了一个我想了一段时间的小测试,结果让我感到非常惊讶。看:

捕获空异常的简单程序,执行 1900 次迭代几乎需要一秒钟:

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

或者,在进行分配之前检查 test == null,同一个程序可以在一秒钟内进行大约 200000000 次迭代。

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

有人详细解释了为什么会有这种巨大的差异吗?

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

注意我正在使用糟糕的 PIV 3.06Ghz 运行它

【问题讨论】:

  • 喜欢你的问题。打算试试看。
  • 使用if (test != null) 会更清楚。
  • @Jon Skeet:不是也快一点吗? (一个 CPU 操作而不是两个)
  • @R。 Bemrose - 编译器很可能在这两种情况下生成相同的 IL

标签: c# performance exception-handling


【解决方案1】:

除非您在调试器中运行,否则 1900 次迭代不可能花费一秒钟。在调试器下运行性能测试是个坏主意。

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

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

我对异常性能的看法:如果您正确使用它们,它们不应该导致严重的性能问题除非无论如何您都处于某种灾难性的情况下(例如,您正试图进行数十万次 Web 服务调用,网络中断)。

在调试器下异常是昂贵的——当然在 Visual Studio 中,无论如何——因为要确定是否闯入调试器等,并且可能会进行任何数量的堆栈分析,否则这是不必要的。无论如何,它们仍然有点昂贵,但你不应该扔掉足够多的东西来引起注意。仍然需要展开堆栈、查找相关的 catch 处理程序等 - 但这应该只在首先出现问题时才会发生。

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

循环体的非异常版本

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

(如 cmets 中所述,JIT 很可能无论如何都会优化它...)

例外版本:

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

您看到性能下降有什么奇怪的吗?

现在将其与更常见的情况进行比较,您需要执行大量工作,可能是 IO、对象创建等 - 可能会引发异常。然后差异就变得不那么显着了。

【讨论】:

  • @devoured elysium:现在可以了;我回答时没有。
  • 真的!我在VS下运行它。现在是 35000 次迭代,但差异仍然很大......另一个测试在相同条件下达到 400000000 次迭代......
  • 另一个测试看起来很容易,但优化为无操作。你还必须记住,异常被设计为异常,即你真的应该比较当你不抛出时会发生什么
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    这个热门问题可能对您也有帮助:How slow are .NET exceptions?

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        编译器执行的优化,我相信它可能是“死代码消除”;还取决于您使用的编译器,后一个程序实际上是在做汇编人员所说的“无操作”。

        【讨论】:

        • 如果这是一个空操作,那么每秒 4 亿次迭代是相当慢的。可能;我想这取决于 32 位机器上的长增量有多慢。
        【解决方案6】:

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

        【讨论】:

        • 在 Jhon Skeet 的回答中查看我的评论。你的号码是多少?
        【解决方案7】:

        这是一个糟糕的微基准测试。

        后一个“优化”循环作为编译时不变量,测试始终为空,因此甚至无需在尝试分配时进行编译。您实际上是在测试一个空循环,每次都抛出异常。

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

        抛出异常的成本相当高(相对于传统的分支控制流)[1],主要是由于 3 个原因:

        1. 所有异常都是引用类型,因此(目前)被堆分配并随后被垃圾回收。
        2. 填充到异常中的堆栈级别(这与堆栈展开的距离成正比 - 您的示例完全无法测量)
        3. 进入异常处理代码会跳过所有好东西,例如分支预测,让当今的深度流水线处理器保持自己做有用的事情

        无论如何,在紧密循环中抛出和捕获异常几乎可以肯定是一个有很大缺陷的设计,但如果你想衡量这种影响,你应该编写一个真正做到这一点的循环。


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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-06-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-10
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多