捕获异常时是否真的有性能损失?

时间:2009-11-20 15:28:39

标签: language-agnostic exception exception-handling

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

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

VS

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

据我所知,除了if被移出正常的代码路径进入异常路径和额外的跳转或两次到达异常代码路径之外没有区别。当它减少主要且经常运行的代码路径中的一些if时,额外的一两跳不会听起来太多。那些异常实际上是慢的吗?或者这是旧编译器的神话还是旧问题?

(我说的是一般的异常。特别是C ++和D等编译语言中的异常;虽然C#也在我的脑海中。)

10 个答案:

答案 0 :(得分:8)

好的 - 我只是进行了一些测试,以确保异常实际上更慢。简介:在我的机器上,每次迭代调用w / return是30个循环。每次迭代抛出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++;

答案 1 :(得分:5)

我不确切知道它有多慢,但是抛出已经存在的异常(比如它是由CLR创建的)并没有太慢,因为你已经引发了构造异常的命中。 ...我相信这是一个异常的 构造 ,它创造了大部分的额外性能......想想看,它必须创建一个堆栈跟踪,(包括读取调试符号以添加行号和内容)并可能捆绑内部异常等。

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

答案 2 :(得分:3)

如果您使用例外来实际控制流量,则可能会受到很大影响。

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

所以不要将异常用于那些不适合做的事情,因为它们比较慢。

答案 3 :(得分:2)

使用例外和一般事物而不用担心性能。然后,完成后,使用性能分析工具测量性能。如果这是不可接受的,你可以找到瓶颈(可能不会是异常处理)并进行优化。

答案 4 :(得分:2)

在C#中,异常会导致每次轻微的性能下降,但这不应该让你不再使用它们。如果你有理由,你应该抛出异常。大多数使用它们有问题的人都说这是因为它们可以破坏程序的流程。

实际上,如果您不使用它们的原因是性能损失,那么您可以更好地花时间优化代码的其他部分。我从来没有遇到这样一种情况,即抛出一个异常导致程序行为如此缓慢以至于必须重新考虑它(抛出异常的行为,而不是代码如何对待它)。

考虑到这一点,尽管如此,我确实尝试使用避免抛出异常的方法。如果可能的话,我将使用TryParse而不是Parse,或者使用KeyExists等。如果您执行相同的操作100次并抛出许多异常,那么少量的低效率可能会增加。

答案 5 :(得分:1)

tl; dr 恕我直言,出于性能原因避免出现异常会触及两类早熟和微观优化。不要这样做。

啊,异常的宗教战争。

对此的各种答案通常是:

  • 通常的口头禅(好的,恕我直言):“在例外情况下使用例外”(IOW,不是“正常”代码路径的一部分)。
    • 如果您的正常用户路径涉及故意使用异常作为控制流机制,那就是气味。
  • 大量细节,没有真正回答原始问题
  • 有人指着microbenchmarks显示像j == 0的i / j这样的东西比检查j == 0
  • 慢了10倍
  • 关于如何一般性地处理应用程序性能的实用答案
    • 通常沿着:
    • 为您的方案制定性能目标(理想情况下与客户合作)
    • 构建它以使其可维护,可读且健壮
    • 运行它并检查目标场景的性能
    • 如果一组场景未达成目标,请使用A PROFILER告诉您花费的时间和去哪里。
    • IOW,任何性能变化,尤其是这样的微观优化,如果没有分析数据来推动这一决定,通常会浪费大量时间。

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

更有趣的链接:

答案 6 :(得分:1)

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

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

答案 7 :(得分:0)

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

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

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

答案 8 :(得分:0)

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

Chris Brumme的文章摘要详细解释了为什么异常明显慢于使用返回代码。在这里复制太长了,你需要做很多阅读才能完全理解为什么。

答案 9 :(得分:-1)

就像连接字符串vs stringbuilder一样。如果你这样做十亿次,那就太慢了。