【问题标题】:Is there really a performance hit when catching exceptions?捕获异常时真的会影响性能吗?
【发布时间】:2010-12-18 18:40:09
【问题描述】:

我问了一个关于exceptions 的问题,我对有人说投掷速度很慢感到非常恼火。我过去问过How exceptions work behind the scenes,我知道在正常的代码路径中没有额外的指令(正如公认的答案所说),但我并不完全相信投掷比检查返回值更昂贵。考虑以下几点:

{
    int ret = func();
    if (ret == 1)
        return;
    if (ret == 2)
        return;
    doSomething();
}

{
    try{
        func();
        doSomething();
    }
    catch (SpecificException1 e)
    {
    }
    catch (SpecificException2 e)
    {
    }
}

据我所知,除了 ifs 被移出正常代码路径进入异常路径和额外的一两次跳转到异常代码路径之外,没有什么区别。当它减少你的主要和更经常运行的代码路径中的几个ifs 时,额外的一两次跳转听起来并不多。那么异常真的很慢吗?或者这是一个神话或旧编译器的旧问题?

(我说的是一般性的异常。具体来说,是 C++ 和 D 等编译语言中的异常;尽管 C# 也在我的脑海中。)

【问题讨论】:

  • 我不愿意将此作为答案发布,但它可能不是额外的跳跃,但我敢打赌与捕获当前正在执行的堆栈帧有关,这会导致速度变慢。你用过“new System.Diagnostics.StackTrace()”吗?很慢。
  • Juliet:糟糕的是,有人添加了 C# 标签并破坏了人们认为我在问的问题。但我认为你的权利。但这就是 C# 的情况,希望更多的人会阅读这个关于 C++ 的问答
  • @Juliet - 您无需捕获堆栈跟踪即可展开异常堆栈。此外,StackTrace() 中的大部分时间都是符号查找。
  • 问题不在于构建它所需的对象创建时间和反射吗?测试一个简单的值肯定比测试一个(可能)大的异常对象更好吗?使用异常而不是简单的 if 语句对我来说很糟糕。 +1 提出问题,而不仅仅是接受教条:)

标签: language-agnostic exception exception-handling


【解决方案1】:

好的 - 我刚刚运行了一个小测试以确保异常实际上更慢。摘要:在我的机器上,每次迭代调用 w/return 是 30 个周期。每次迭代的 throw w/catch 是 20370 个周期。

所以要回答这个问题 - 是的 - 抛出异常很慢。

这是测试代码:

#include <stdio.h>
#include <intrin.h>

int Test1()
{
    throw 1;
//  return 1;
}


int main(int argc, char*argv[])
{
    int result = 0;
    __int64 time = 0xFFFFFFFF;
    for(int i=0; i<10000; i++)
    {
        __int64 start = __rdtsc();
        try
        {
            result += Test1();
        }
        catch(int x)
        {
            result += x;
        }
        __int64 end = __rdtsc();
        if(time > end - start)
            time = end - start;
    }

    printf("%d\n", result);
    printf("time: %I64d\n", time);
}

由 op 编写的替代 try/catch

try
{
        if(Test1()!=0)
                result++;
}
catch(int x)
{
        result++;

【讨论】:

  • 是的!终于有人有了真正的测试和真正的答案。
  • 我想知道它在做什么......但这至少告诉我它是一个巨大的成功。
  • 这是 C# 中的另一个。它也慢得多。我想知道为什么期望与返回值如此不同。 pastie.org/707883
  • 我发现将调试器附加到我的 C# 程序可以极大地改变基准测试的结果。我假设 C 程序可能会受到相同的影响,所以我希望你在关闭调试器的情况下运行你的基准测试。
  • @Greg - 我确实在关闭调试器的情况下运行它...在调试器下运行会得到 10,000 个必须忽略的第一次机会异常。
【解决方案2】:

我不知道它到底有多慢,但是抛出一个已经存在的异常(比如它是由 CLR 创建的)并不会慢很多,因为你已经遭受了构造异常的影响。 ...我相信是异常的构造造成了大部分额外的性能损失...想想看,它必须创建一个堆栈跟踪,(包括读取调试符号以添加行号和内容)并可能捆绑内部异常等。

实际上抛出异常只会添加额外的代码来遍历堆栈以找到适当的 catch 子句(如果存在)或将控制权转移到 CLR 未处理的异常处理程序......这部分对于非常深的堆栈来说可能是昂贵的, 但是如果 catch 块就在你扔它的方法的底部,例如,那么它会相对便宜。

【讨论】:

  • 至少在 C++ 中这是绝对错误的。请参阅我的答案以获取示例-我正在抛出“ 0”-这是您可以获得的最便宜的结构。就读取调试符号而言,如果它不仅仅存储跟踪位置并懒惰地处理符号,我会感到非常惊讶。
  • @Aaron,无论如何我都不是 C++ 专家,但是在你的代码 sn-p 中,使用 throw(1) 语句,它不是在构造一个异常对象吗?即使您只是抛出(1),编译器是否不必将该整数值汇总为某种类型的异常?当您在另一种情况下使用返回值时,您是否没有评论放置 throw(1) 行?只是为了笑,(我想知道结果会是什么?) - 尝试构造一个异常而不抛出它,返回一个操作码给调用者,看看需要多长时间..
  • 这将隔离由于构造异常而导致的性能损失部分 frpm 由于抛出它而导致的命中...
  • 好的,来自 msdn。 "3. 如果在受保护部分的执行过程中或在受保护部分调用的任何例程中(直接或间接)抛出异常,则从抛出操作数创建的对象创建异常对象。(这意味着复制构造函数可能涉及到。)“这是否意味着你的行读取 throw(1) 会导致执行一个构造函数,该构造函数实例化一个异常对象?
【解决方案3】:

如果您使用异常来实际控制流程,它可能会大受欢迎。

我正在挖掘一些旧代码,看看为什么它运行得这么慢。在一个大循环中,它不是检查 null 并执行不同的操作,而是捕获 null 异常并执行替代操作。

因此,不要将异常用于它们原本不打算做的事情,因为它们速度较慢。

【讨论】:

    【解决方案4】:

    使用异常和一般任何东西,而不用担心性能。然后,完成后,使用分析工具测量性能。如果不能接受,你可以找到瓶颈(可能不是异常处理)并优化。

    【讨论】:

    • 有人大发雷霆,或者他们没有听说过过早优化?
    • 重写使用返回码来使用异常或反之亦然的应用程序需要付出巨大的努力。这真的是一个应该提前做出的决定,因为以后很难改变。
    • 这对我来说确实是一个全新的视角,谢谢。不过,我怀疑异常通常会成为瓶颈。
    【解决方案5】:

    在 C# 中,引发异常确实会对性能造成非常轻微的影响,但这不应该让您害怕使用它们。如果你有原因,你应该抛出一个异常。大多数使用它们有问题的人都说原因是因为它们会破坏程序的流程。

    真的,如果您不使用它们的原因是性能受到影响,那么您可以更好地花时间优化代码的其他部分。我从来没有遇到过抛出异常导致程序运行缓慢以至于必须重新分解它的情况(抛出异常的行为,而不是代码如何处理它)。

    再想一想,话虽如此,我确实尝试使用避免抛出异常的方法。如果可能的话,我会使用 TryParse 而不是 Parse,或者使用 KeyExists 等。如果您执行相同的操作 100 次以上并抛出许多异常,则可能会增加少量的低效率。

    【讨论】:

      【解决方案6】:

      是的。异常会使您的程序在 C++ 中变慢。不久前我创建了一个 8086 CPU 模拟器。在代码中,我使用了 CPU 中断和故障的异常。我做了一个大的复杂循环的小测试用例,运行了大约 2 分钟的模拟操作码。当我通过分析器运行此测试时,我的主循环正在对 gcc 的“异常检查器”函数进行大量调用(实际上有两个不同的函数与此相关。但是我的测试代码最后只抛出了一个异常.) 我相信每次都在我的主循环中调用这些异常函数(这是我有 try{}catch{} 部分的地方。)。异常函数花费了我大约 20% 的运行时速度。(代码在那里花费了 20% 的时间)。异常函数也是分析器中调用次数最多的第 3 和第 4 函数...

      所以是的,即使没有持续抛出异常,使用异常也会很昂贵。

      【讨论】:

        【解决方案7】:

        tl;dr 恕我直言,出于性能原因避免异常会影响过早优化和微优化这两个类别。不要这样做。

        啊,例外的宗教战争。

        对此的各种类型的答案通常是:

        • 通常的口头禅(很好,恕我直言):“在异常情况下使用异常”(IOW,不是“正常”代码路径的一部分)。
          • 如果您的正常用户路径涉及故意使用异常作为控制流机制,那就太难了。
        • 大量细节,没有真正回答原始问题
        • 有人指着微基准测试表明,像 i/j 这样 j == 0 的东西,捕捉 div-by-zero 比检查 j == 0 慢 10 倍
        • 关于如何处理一般应用程序性能的实用答案
          • 通常是这样的:
          • 为您的场景制定性能目标(最好与客户合作)
          • 构建它,使其可维护、可读且健壮
          • 运行它并检查目标场景的性能
          • 如果一组场景没有达到目标,请使用 PROFILER 告诉您您的时间都花在了哪里,然后从那里开始。
          • IOW,任何性能更改,尤其是像这样的微优化,在没有分析数据驱动该决策的情况下进行,通常都是浪费时间。

        请记住,您的性能获胜通常来自算法更改(向表添加索引以避免表扫描,将具有较大 n 的内容从 O(n^3) 移动到 O(n ln n) 等。 )。

        更多有趣的链接:

        【讨论】:

          【解决方案8】:

          如果您想知道异常在 Windows SEH 中是如何工作的,那么我相信this article by Matt Pietrik 被认为是权威参考。这不是轻读。如果您想将此扩展到异常在 .NET 中的工作方式,则需要阅读 this article by Chris Brumme,这绝对是权威参考。读起来也不轻松。

          Chris Brumme 文章的摘要详细解释了为什么异常比使用返回码慢得多。在这里复现太长了,在完全理解原因之前,您需要阅读大量内容。

          【讨论】:

            【解决方案9】:

            部分答案是编译器并没有非常努力地优化异常代码路径。

            • catch 块是对编译器以牺牲异常代码路径为代价积极优化非异常代码路径的一个非常强烈的提示。为了可靠地提示编译器 if 语句的哪个分支是例外分支,您需要配置文件引导优化。

            • 异常对象必须存储在某个地方,并且因为抛出异常意味着堆栈展开,所以它不能在堆栈上。编译器知道异常很少见——因此优化器不会做任何可能减慢正常执行的事情——比如保持寄存器或任何类型的“快速”内存可用,以防它需要将异常放入其中。您可能会发现出现页面错误。相反,返回码通常以寄存器结束(例如 EAX)。

            【讨论】:

              【解决方案10】:

              这就像连接字符串与字符串生成器。如果你做十亿次,它只会很慢。

              【讨论】:

              • 我在问为什么。我很确定它不像连接字符串
              • 因为它必须展开堆栈、创建异常对象、与可捕获异常列表进行比较等。显然我是在比较连接字符串的效果,而不是真正发生的情况
              • 顺便说一句,我没有 -1 你。通过 if(cond) return 离开函数时会发生展开;比较列表听起来像是开关的许多ifs。所以我还没有看到区别。
              • 别担心,我总是得到-1d ;) 我明白你对堆栈的看法。我认为比较异常比比较整数更重要。但 IMO 不太可能对现实世界产生重大影响。
              猜你喜欢
              • 2011-01-10
              • 1970-01-01
              • 1970-01-01
              • 2011-09-25
              • 1970-01-01
              • 2021-09-25
              • 1970-01-01
              • 1970-01-01
              • 2020-06-08
              相关资源
              最近更新 更多