C++0x抛出异常的成本

14

22
在Java中,不应该使用异常来处理一般的逻辑控制。 - Bill the Lizard
8
@Bill: 将 "in Java" 改为 "EVER"。它们被称为 "异常",因为它们是异常情况。 - Pesto
1
你认为C++0x异常与其他异常有何不同?我认为大部分差异在于实现方式。 - David Thornley
1
异常处理会有一定的成本,但这不应该是你担心的问题。你应该关注的是使用异常和不使用异常(返回错误代码并在每个回调级别进行检查)获取相同信息的成本差异。我敢打赌,成本大致相同,区别在于代码更加清晰(从而更易维护)。 - Martin York
1
你能解释一下我的基准测试应该如何修改,以便你有赢得那个赌注的机会吗?我的“thrower”函数只是进行调用并返回void。我的“returner”函数进行调用,检查返回值,并相应地返回成功或失败。显然,“thrower”函数较慢,尽管不足以影响大多数应用程序。 - Steve Jessop
显示剩余2条评论
10个回答

36
#include <iostream>
#include <stdexcept>

struct SpaceWaster {
    SpaceWaster(int l, SpaceWaster *p) : level(l), prev(p) {}
    // we want the destructor to do something
    ~SpaceWaster() { prev = 0; }
    bool checkLevel() { return level == 0; }
    int level;
    SpaceWaster *prev;
};

void thrower(SpaceWaster *current) {
    if (current->checkLevel()) throw std::logic_error("some error message goes here\n");
    SpaceWaster next(current->level - 1, current);
    // typical exception-using code doesn't need error return values
    thrower(&next);
    return;
}

int returner(SpaceWaster *current) {
    if (current->checkLevel()) return -1;
    SpaceWaster next(current->level - 1, current);
    // typical exception-free code requires that return values be handled
    if (returner(&next) == -1) return -1;
    return 0;
}

int main() {
    const int repeats = 1001;
    int returns = 0;
    SpaceWaster first(1000, 0);

    for (int i = 0; i < repeats; ++i) {
        #ifdef THROW
            try {
                thrower(&first);
            } catch (std::exception &e) {
                ++returns;
            }
        #else
            returner(&first);
            ++returns;
        #endif
    }
    #ifdef THROW
        std::cout << returns << " exceptions\n";
    #else
        std::cout << returns << " returns\n";
    #endif
}

米老鼠基准测试结果:

$ make throw -B && time ./throw
g++     throw.cpp   -o throw
1001 returns

real    0m0.547s
user    0m0.421s
sys     0m0.046s

$ make throw CPPFLAGS=-DTHROW -B && time ./throw
g++  -DTHROW   throw.cpp   -o throw
1001 exceptions

real    0m2.047s
user    0m1.905s
sys     0m0.030s

在这种情况下,将异常抛出到1000个堆栈级别而不是正常返回大约需要1.5毫秒的时间。这包括进入try块,在某些系统上我相信执行时是免费的,在其他系统上每次进入try都会产生成本,在其他系统上只有在进入包含try的函数时才会产生成本。对于更可能的100个堆栈级别,我将重复次数增加到了10k,因为所有内容都快了10倍。所以异常成本为0.1ms。
对于10,000个堆栈级别,它是18.7s与4.1s之间,因此异常额外花费了大约14ms。因此,对于这个例子,我们正在查看每个堆栈级别的相当一致的1.5微秒的开销(其中每个级别都在析构一个对象)。
显然,C++0x没有为异常(或任何其他东西,除了算法和数据结构的大O复杂度)指定性能。我认为它不会以任何会严重影响许多实现的方式改变异常,无论是积极的还是消极的。

1
我手头没有MSVC(这是在Windows XP上使用g++和cygwin运行的)。同时,我也非常谨慎地声称这是一个好的基准测试,因为对于简单的东西,你永远不知道某个聪明的编译器是否会找到优化递归的方法。曾经在一次遵从性测试中发生过这种情况,这个测试应该检查系统是否具有N级递归的堆栈。在一起铺设结构体是一个相当可靠的防止它被优化掉的方法,但我不做任何承诺,并且结果应该在每个平台上都要小心解释。 - Steve Jessop
2
我认为测试一个实现并不比表达你对某事的看法更有价值(无恶意)。如果你的想法是基于事实的,那么它们同样有效,只是方式不同。这个回答将成为“gcc和msvc,x86未优化构建”的事实标准答案。但是,正如你所想象的那样,还有很多编译器和很多构建配置 :) 无论如何,我对这个研究回答给予+1的支持。 - Johannes Schaub - litb
1
@darth:针对你的第二点,问题是“我们是否应该使用异常来处理一般逻辑”。我认为可以合理地假设,如果这样做(而这个测试旨在研究如果这样做会发生什么),那么函数只会在成功时返回,并在失败时抛出异常。因此,没有需要检查的失败代码。我并不是说我总是喜欢结果代码,但问题是它的性能如何。答案似乎是,“在这个过度简化的基准测试中,对于许多实际目的来说还不错”。如果你有更好的基准测试,请拿出来看看 :-) - Steve Jessop
当我将异常类型从std::logic_error更改为my_except(struct {})时,测试速度提高了4倍以上。因此,大部分额外时间都用于创建异常对象。 - user1485360
我测试了错误发生率为0.1%的情况,使用更浅但更复杂的函数,以便它们实际上必须检查错误值。同时,将空格浪费替换为字符串。是的,C ++异常在抛出时速度较慢,但当它们不抛出异常时会更快,从而进行补偿。 Translated text: 我测试了错误发生率为0.1%的情况,使用更浅但更复杂的函数,以便它们实际上必须检查错误值。同时,将空格浪费替换为字符串。是的,C ++异常在抛出时速度较慢,但当它们不抛出异常时会更快,从而进行补偿。 - Mooing Duck
显示剩余8条评论

14

异常性能非常依赖于编译器。您需要对应用程序进行剖析以查看是否存在问题。通常情况下,不应该成为问题。

您真的应该将异常用于“异常情况”,而不是一般逻辑处理。异常非常适合将代码的正常路径和错误路径分离。


2
我也点赞。在正常逻辑中使用异常可能会导致“异常意大利面条”,需要花费很长时间才能弄清楚捕获的位置。特别是在多态代码中,当您甚至不知道ListenerRegistry的哪个实现是您的调用者时,除了运行它并查看当前配置中获取的堆栈跟踪之外,还需要做很多工作。如果异常仅用于预期无法恢复的情况,则大多数代码级别甚至不会考虑尝试从中恢复 :-) - Steve Jessop

11
我认为问题的提出方式不正确。关键是比较异常情况与其它可选方案的成本而非单纯地计算异常情况的成本。因此,你需要测量异常情况的成本,并将其与返回错误代码>>>并在每个堆栈展开层级上检查错误代码进行比较。
同时请注意,当你拥有全部控制权时,不应使用异常情况。在类内部返回错误代码可能是一种更好的技术。异常应该用于在运行时传输控制,当你无法确定对象在运行时将如何(或在何种上下文中)被利用时。
基本上,它应该用于将控制转移到具有足够上下文的更高级别的上下文,以便该对象了解如何处理异常情况。
考虑到这种使用原则,我们可以看到异常将用于将控制传递到堆栈帧中的多个级别。现在考虑一下为了将错误代码传回同一调用堆栈所需编写的额外代码。尝试并协调所有不同类型的错误代码时添加的额外复杂性。
鉴于此,你可以看到异常情况如何极大地简化了代码的流程,并且你可以看到代码流程的继承复杂性。那么问题就变成了异常情况是否比需要在每个堆栈帧中执行的复杂错误条件测试更昂贵。
答案始终取决于情况(如果需要,请同时进行剖析并使用最快的方式)。
但是,如果速度不是唯一的成本。可维护性是可以衡量的成本。使用这种成本指标,异常始终胜出,因为它们最终使代码的控制流只与需要完成的任务相关而不包含错误控制。

现在考虑你需要编写的额外代码来传递错误代码。我的numpty基准尝试模拟这个过程(请参见“returner”函数的递归调用)。我还没有检查编译器是否进行了优化。因此,我对于捕获异常的估计可能是一个过高的估计,即在上面的3个级别中。我的“throw”循环肯定比“返回错误”循环慢得多。当然,在添加任何真正的工作之前。我希望能够提出任何建议,以使我的“returner”循环更准确地反映“检查返回代码并退出”的错误处理。 - Steve Jessop
2
特别是,“返回者”在成功情况下可能会更慢,这取决于编译器选择如何发出分支。通常你不会听到“异常!呸!极其缓慢!”的人群讨论他们如何在正常的成功操作中引入额外的分支(与基于异常的错误处理相比)。 - Steve Jessop

7

我曾经创建过一个x86仿真库,并使用异常来处理中断等。这是个坏主意。即使我没有抛出任何异常,它也会对我的主循环产生很大的影响。以下是我的主循环的代码:

try{
    CheckInterrupts();
    *(uint32_t*)&op_cache=ReadDword(cCS,eip);
    (this->*Opcodes[op_cache[0]])();
    //operate on the this class with the opcode functions in this class
    eip=(uint16_t)eip+1;

}
//eventually, handle these and do CpuInts...
catch(CpuInt_excp err){
    err.code&=0x00FF;
    switch(err.code){

将该代码放入try块中的开销使异常函数成为CPU时间使用率前5名中的2个。

对我来说,这太昂贵了。


是的,确实如此。当我对其进行性能分析时,我执行的是非常简单的代码,从未抛出异常(除了最后一个)。然而,try和catch仍然需要取消堆栈。并且为了澄清,我指的是两个异常函数之一(在gcc中有两个),它是按照消耗CPU时间最多的前5个函数之一。 - Earlz
请记住,对于每个堆栈帧,您必须调用所有本地对象析构函数并销毁本地内存。每个帧将消耗相当大的内存。这就是为什么它们应该保留给“异常”情况的原因。 - Chris K

6
在C++0x中,异常不应该比C++03中的异常快或慢。这意味着它们的性能完全取决于实现。Windows使用完全不同的数据结构来实现32位与64位及Itanium与x86上的异常处理。Linux也不能保证只采用一种实现方式。这取决于编译器、运行时库、操作系统和CPU架构。有几种流行的方式来实现异常处理,它们都有优点和缺点。因此,这并不取决于语言(C++03 vs 0x),而是取决于编译器、运行时库、操作系统和CPU架构。

4
我认为C++0x并没有对C++异常的工作方式进行任何增强或更改。关于一般建议,请参见这里

2

人们会认为它们在C++03中的性能大致相同,即“非常慢”!而且,不管是哪种语言,由于try-catch-throw结构,异常应该只在特殊情况下使用。如果您在Java中使用throw来控制程序流程,则说明您做错了什么。


6
在我的笔记本电脑和C++编译器上,“非常缓慢”的取值约为1毫秒。显然,如果这是你最内层的循环,那就是个问题。我认为不使用异常处理非错误情况的原因在于它会使你的代码控制流更难以理解,而不是担心性能方面的问题。 - Steve Jessop
@onebyone - 这并不是关于性能的虚假信息,性能损失是真实且非常显著的,并且异常会对实时应用程序造成严重影响。 - Not Sure
5
如果你在不知道使用异常会导致应用程序运行速度降低多少百分比的情况下说“异常成本非常显著”,那么这就是恐惧、不确定性和怀疑(FUD)。有时它确实很显著,但通常并不是。而且你不能以“它会破坏实时应用程序”为理由不使用特定的语言功能,因为(a)几乎所有应用程序都不是实时的,(b)几乎所有语言功能都可能破坏实时应用程序,因为无法预测缓存未命中等问题。 - Steve Jessop

0

@Steve Jessop已经发布了事实上的答案,但我仍然认为有一件事情:尝试不同的优化级别,比如大多数产品常用的“-O2”。


-2

异常处理是一项昂贵的功能,因为抛出/捕获意味着需要执行额外的代码来确保堆栈展开和捕获条件评估。

据我从一些阅读中了解到,例如对于Visual C++,代码中嵌入了一些相当复杂的结构和逻辑来确保这一点。由于大多数函数可能调用其他可能引发异常的函数,因此即使在这些情况下也可能存在堆栈展开的一些开销。

然而,在考虑异常开销之前,最好在任何优化操作之前测量异常使用对您的代码的影响。避免过度使用异常应该可以防止异常处理造成的重大开销。


4
你意识到如果你正常返回,调用栈仍然需要展开吗? - Steve Jessop
2
异常基本上是免费的,只有在存在异常传播时,放置try catch块的额外成本才可以忽略不计。由于异常而进行的解开操作成本更高,但与编写代码以传递错误代码所需的成本相同。因此,声称它很昂贵是完全错误的。 - Martin York

-2

想象一下,铃声响起,计算机停止接受任何输入三秒钟,然后有人踢了用户的头。

这就是异常的代价。如果它可以防止数据丢失或机器着火,那么它的代价是值得的。否则,可能不值得。

编辑:由于这篇文章被踩了(还有一个赞,所以对我来说是+8!),我将用更少的幽默和更多的信息来澄清上面的内容:异常,至少在C++领域,需要RTTI和编译器以及可能的操作系统魔法,这使得它们的性能成为一个巨大的不确定性黑洞。(你甚至不能保证它们会触发,但这些情况发生在其他更严重的事件中,比如内存耗尽、用户杀死进程或机器实际着火。)因此,如果你使用它们,应该是因为你想从本来会导致某些可怕事情发生的情况中优雅地恢复过来,但是这种恢复不能对运行性能有任何期望(无论对于你特定的应用程序来说是什么)。

因此,如果你要使用异常,你不能假设任何关于性能影响的事情。


6
什么?异常并不需要操作系统或编译器的神奇技巧,它们是很容易理解的。不能保证触发?对不起,这完全错误。异常可以极大地帮助管理错误和意外情况,并通过分离正常路径和错误路径来创建更可维护的代码。你绝不能说它们几乎没有价值。并非所有应用程序都需要高性能或实时性能,而且通常在程序处于错误状态时你并不关心性能。 - Brian Neal
2
当异常已经激活时,如果发生更多的错误,它可能不会“触发”。但此时程序已经崩溃了,无论如何都无法正常工作。这并不是避免使用异常的理由。 - Brian Neal
1
需要在故障下保持强大的系统,如果还想使用异常处理,可以使用堆栈边界等方法。中断处理程序通常也是在单独的堆栈上运行,以便在关键的资源不足情况下仍然能够正常工作。 - Steve Jessop
3
有些人因为“不保证触发”这句无意义的话而想要踩你的帖子,但除此之外,你的帖子其他部分都讲得很有道理(因为异常的性能表现是无法假设的)。虽然确实存在异常不被触发的可能性,但与此同时,调用函数时也不能保证一定会被执行。在那种情况下,你可能会遇到栈空间耗尽的问题,或者用户关掉了电脑。说异常不保证触发是极端误导的。 - jalf
1
你的回答仍然有很多FUD。“编译器魔法”,“操作系统魔法”。拜托,给些例子。许多程序可以从使用异常中受益。说它们只值得用来防止机器着火,因为它们的性能成本太高是荒谬的。你做过大规模的C++开发吗?首先你说它们太昂贵,然后你又说你不能假设任何关于它们性能的事情。抱歉,这个回答不好。 - Brian Neal
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接