【问题标题】:Cost of throwing C++0x exceptions抛出 C++0x 异常的代价
【发布时间】:2010-11-04 08:44:48
【问题描述】:

在 C++0x 中抛出异常对性能有何影响?这个编译器依赖多少?这和问what is the cost of entering a try block, even if no exception is thrown不一样。

我们是否应该期望像在 Java 中那样将异常更多地用于一般逻辑处理?

【问题讨论】:

  • 你不应该使用异常来处理 Java 中的一般逻辑。
  • @Bill: s/in Java/EVER/ 它们被称为“例外”是有原因的;他们很特别。
  • 为什么你认为 C++0x 异常与其他异常会有这么大的不同?我认为大多数变化将在实现之间。
  • 例外情况会产生费用。但这不应该是你担心的。它应该是使用异常和不使用异常获取相同信息之间的成本差异(因此返回错误代码 >>>>AND
  • 您能解释一下我的基准应该如何修改,以便您有机会赢得该赌注吗?我的“thrower”函数只是调用并返回 void。我的“returner”函数进行调用,检查返回值,并相应地返回成功或失败。 “thrower”函数显然要慢一些,但还不足以影响大多数应用程序。

标签: c++ performance exception


【解决方案1】:
#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.7 秒与 4.1 秒,因此异常的额外成本约为 14 毫秒。因此,对于这个示例,我们正在查看每层堆栈 1.5us 的相当一致的开销(其中每一层都破坏一个对象)。

显然 C++0x 没有指定异常的性能(或其他任何东西,除了算法和数据结构的大 O 复杂度)。我不认为它改变异常的方式会严重影响许多实现,无论是正面还是负面。

【讨论】:

  • 这是一个很好的答案,因为数据的基准和解释比“我认为 bla bla bla”更能说明问题。如果您可以使用 Windows 和某些版本的 Visual Studio 重复这些操作,那么这将是异常处理的“事实上”答案
  • 我手头没有 MSVC(这是在 Windows XP 上使用 g++ 和 cygwin 运行的)。对于声称这是一个很好的基准,我也会非常谨慎,因为你永远不会用简单的东西知道一些 smart-alec 编译器是否会弄清楚如何优化递归中的一种或另一种案件。如果在本应检查系统是否具有 N 级递归堆栈的一致性测试中发生过这种情况。随时放置结构是一种相当可靠的防止它的方法,但我不做任何承诺,结果应该在每个平台上都非常仔细地进行交互。
  • 使用 Windows/VS 进行测试是不够的。 win32和win64的异常实现完全不同。 (以及在 x86 和 Itanium 上运行的 Windows)
  • 我不认为(没有双关语)测试一个实现比说出你对某事的想法更有价值。如果你的想法是根据事实来判断的,那么它们同样有效,但方式不同。这将是“gcc 和 msvc,x86 的未优化构建”的实际答案。但正如您想象的那样,有更多的编译器和更多的构建配置:) 无论如何,我为这个研究答案 +1
  • @darth:第二点,问题是“我们是否应该使用异常来处理一般逻辑处理”。我认为可以公平地假设如果有人这样做(并且这个测试旨在调查如果这样做会发生什么),那么函数只会在成功时返回,并在失败时抛出。因此,没有要检查的故障代码。我并不是说我总是喜欢生成的代码,但问题是它会如何执行。答案似乎是,“在这个过于简单的基准测试中,对于许多实际目的来说还不错”。如果你有更好的基准,让我们看看:-)
【解决方案2】:

异常性能非常依赖于编译器。您必须分析您的应用程序以查看它是否存在问题。一般来说,它不应该是。

您确实应该对“异常情况”使用异常,而不是一般的逻辑处理。异常是通过代码分隔正常路径和错误路径的理想选择。

【讨论】:

  • 我也是+1。对正常逻辑使用异常可能会导致“异常意大利面”,这是一项工作半,只是为了弄清楚事情在哪里被捕获。特别是对于多态代码,当您甚至可能不知道 ListenerRegistry 的哪个实现是您的调用者时,您无法运行它并查看您在当前配置中获得的堆栈跟踪。如果异常仅用于预期无法恢复的情况,那么大多数级别的代码甚至都不会考虑尝试从它们中恢复:-)
【解决方案3】:

我基本上认为问错了问题。
什么是例外的成本没有用,更有用的是例外相对于替代方案的成本。因此,您需要测量异常成本并将其与返回的错误代码进行比较 >>>AND

还请注意,当您可以控制一切时,不应使用异常。在返回错误代码的类中可能是一种更好的技术。当您无法确定在运行时将如何(或在什么上下文中)使用您的对象时,应使用异常在运行时转移控制。

基本上,它应该用于将控制权转移到更高级别的上下文,其中具有足够上下文的对象将了解如何处理异常情况。

鉴于这种使用原则,我们看到异常将用于将控制权转移到堆栈帧中的多个级别。现在考虑您需要编写的额外代码以将错误代码传递回相同的调用堆栈。当错误代码可能来自多个不同方向并尝试协调所有不同类型的错误代码时,请考虑额外的复杂性。

鉴于此,您可以看到异常如何极大地简化代码流程,并且您可以看到代码流程的复杂性。那么问题就变成了天气异常比需要在每个堆栈帧上执行的复杂错误条件测试更昂贵。

答案始终取决于(如果需要,请同时进行配置文件和使用 quickist)。

但如果速度不是唯一的成本。
可维护性是可以衡量的成本。使用这种成本度量异常总是会获胜,因为它们最终使代码的控制流只针对需要完成的任务,而不是任务和错误控制。

【讨论】:

  • “现在考虑你需要编写的额外代码来传递错误代码”我的 numty 基准测试试图模拟这一点(参见“returner”函数的递归调用)。我还没有检查编译器是否对其进行了优化。所以我估计 13us 的异常捕获 3 级可能是高估了。我的“抛出”循环肯定比“返回错误”循环慢得多。当然,这是在添加任何实际工作之前。对于如何使我的“返回器”循环更准确地反映“检查返回代码并退出”错误处理的任何建议,我将不胜感激。
  • 特别是,在 success 的情况下,“returner”可能会更慢,这取决于编译器选择发出分支的方式。您通常不会从“例外!糟糕!冰川缓慢!”中听到太多消息。围观他们如何在正常、成功的操作中引入额外的分支(与基于异常的错误处理相比)。
【解决方案4】:

我曾经创建了一个 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 个。

那对我来说很贵

【讨论】:

  • 是的,确实如此。当我分析它时,我正在执行非常简单的代码,它从不抛出异常(最后除外),但尝试和捕获仍然需要解开堆栈。并且我的意思是两个异常函数(有两个使用 gcc)是列出的前 5 个函数之一,按消耗的 CPU 时间排序
  • 请记住,对于每个堆栈帧,您必须调用所有本地对象析构函数并销毁本地内存。每个帧都将消耗不可忽略的内存量。这就是为什么它们应该为“特殊”情况保留。
【解决方案5】:

C++0x 中的异常没有理由比 C++03 中的更快或更慢。这意味着它们的性能完全依赖于实现。 Windows 使用完全不同的数据结构在 32 位和 64 位以及 Itanium 和 x86 上实现异常处理。 Linux 也不能保证只坚持一种实现。这取决于。有几种流行的方式来实现异常处理,它们都有优点和缺点。

因此它不依赖于语言(c++03 vs 0x),而是依赖于编译器、运行时库、操作系统和 CPU 架构。

【讨论】:

    【解决方案6】:

    我认为 C++0x 不会对 C++ 异常的工作方式进行任何添加或更改。对于一般建议,请查看here

    【讨论】:

      【解决方案7】:

      可以想象它们的性能与 C++03 中的性能大致相同,即“非常慢!”不,由于 try-catch-throw 结构,在任何语言中都应该在特殊情况下使用异常。如果您在 java 中使用 throw 进行程序流控制,那么您做错了。

      【讨论】:

      • 对于“非常慢!”的值在我的笔记本电脑和 c++ 编译器上大约 1ms。显然,如果那在您最内层的循环中,那将是一个问题。我认为不对非错误情况使用异常的原因是它使您的代码的控制流更难遵循,而不是关于性能的 FUD。
      • @onebyone - 这不是关于性能的 FUD,对性能的影响是真实的并且非常重要,并且异常会对实时应用程序造成严重破坏。
      • 如果您说异常的成本“非常重要”,但不知道使用它们会导致实际应用程序运行速度减慢多少百分比,这就是 FUD。有时它很重要,但通常不是。而且您不能使用“它会破坏实时应用程序”作为不使用特定语言功能的理由,因为(a)几乎所有应用程序都不是实时的,并且(b)几乎所有语言功能都可能破坏实时应用程序,通过使无法预测缓存未命中之类的东西。
      【解决方案8】:

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

      【讨论】:

        【解决方案9】:

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

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

        但是,在考虑异常开销之前,最好在执行任何优化操作之前衡量代码中异常使用的影响。避免异常过度使用应该可以防止异常处理大量开销。

        【讨论】:

        • 你意识到如果你正常返回,堆栈仍然需要展开?
        • 异常基本上是免费的,而没有异常传播放置 try catch 块的额外成本充其量是可以忽略的。由于异常而展开的成本更高,但不会超过编写代码以将错误代码传回的成本。所以说它贵的说法是完全错误的。
        【解决方案10】:

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

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

        编辑:因为这得到了一个反对票(加上一个赞成票,所以对我来说 +8!),我将用更少的幽默和更多的信息来澄清上述内容:异常,至少在 C++ 领域,需要 RTTI 和编译器和可能是操作系统的魔法,这使得它们的表现成为一个巨大的不确定性黑洞。 (你甚至不能保证它们会触发,但是这些情况发生在其他更严重的事件中,比如内存不足或用户只是杀死进程或机器实际上着火了。)所以如果你使用它们,它应该是因为您希望从否则会导致可怕事情发生的情况中优雅地恢复,但该恢复不能期望以高性能运行(无论对于您的特定应用程序可能是什么)。

        因此,如果您要使用异常,则不能对性能影响做出任何假设。

        【讨论】:

        • 什么?异常不需要操作系统魔法,也不需要编译器魔法。他们很好理解。不保证开火?抱歉,这是完全错误的。异常对于管理错误和意外情况以及通过分离正常路径和错误路径来创建更可维护的代码非常有用。您根本不能说它们几乎不值得付出代价。并非所有应用程序都需要高性能或实时性能,而且当您的程序处于错误状态时,您通常并不关心性能。
        • 当然,如果在异常已经处于活动状态时发生更多坏事,它可能不会“触发”。但是到那时游戏结束了,你的程序很可能会崩溃。 IE。什么都行不通。这不是避免例外的理由。
        • 需要在故障下保持稳健的系统如果还想使用异常,则可以使用堆栈边距等。与中断处理程序(通常)在单独的堆栈上运行的方式相同,因此它们仍然可以在严重的资源不足情况下工作。
        • 由于关于“不保证会触发”的废话,很想投反对票,但您的其余帖子是有道理的(您不能对异常的性能做出太多假设),所以我不会吨。的确,从技术上讲,不能保证触发异常,但是也不能保证在调用函数时对其进行评估。你也可能用完那里的堆栈空间。用户可以关闭计算机。说他们没有保证是极端的误导。
        • 你的回答还是很多 FUD。 “编译器魔法”、“操作系统魔法”。来吧。请举例。许多程序可以从使用异常中受益。说它们只是为了防止你的机器着火是值得的,因为它们的性能成本太高了,这是荒谬的。你曾经做过任何大规模的 C++ 开发吗?首先你说它们太贵了,然后你说你不能对它们的性能做出任何假设。对不起,不好的答案。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-14
        • 2017-07-01
        • 2015-08-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多