【问题标题】:The Cost of C++ Exceptions and setjmp/longjmpC++ 异常和 setjmp/longjmp 的代价
【发布时间】:2015-10-07 19:53:54
【问题描述】:

我编写了一个测试来测量线程中 C++ 异常的成本。

#include <cstdlib>
#include <iostream>
#include <vector>
#include <thread>

static const int N = 100000;

static void doSomething(int& n)
{
    --n;
    throw 1;
}

static void throwManyManyTimes()
{
    int n = N;
    while (n)
    {
        try
        {
            doSomething(n);
        }
        catch (int n)
        {
            switch (n)
            {
            case 1:
                continue;
            default:
                std::cout << "error" << std::endl;
                std::exit(EXIT_FAILURE);
            }
        }
    }
}

int main(void)
{
    int nCPUs = std::thread::hardware_concurrency();
    std::vector<std::thread> threads(nCPUs);
    for (int i = 0; i < nCPUs; ++i)
    {
        threads[i] = std::thread(throwManyManyTimes);
    }
    for (int i = 0; i < nCPUs; ++i)
    {
        threads[i].join();
    }
    return EXIT_SUCCESS;
}

这是我最初为了好玩而写的 C 版本。

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <glib.h>

#define N 100000

static GPrivate jumpBuffer;

static void doSomething(volatile int *pn)
{
    jmp_buf *pjb = g_private_get(&jumpBuffer);

    --*pn;
    longjmp(*pjb, 1);
}

static void *throwManyManyTimes(void *p)
{
    jmp_buf jb;
    volatile int n = N;

    (void)p;
    g_private_set(&jumpBuffer, &jb);
    while (n)
    {
        switch (setjmp(jb))
        {
        case 0:
            doSomething(&n);
        case 1:
            continue;
        default:
            printf("error\n");
            exit(EXIT_FAILURE);
        }
    }
    return NULL;
}

int main(void)
{
    int nCPUs = g_get_num_processors();
    GThread *threads[nCPUs];
    int i;

    for (i = 0; i < nCPUs; ++i)
    {
        threads[i] = g_thread_new(NULL, throwManyManyTimes, NULL);
    }
    for (i = 0; i < nCPUs; ++i)
    {
        g_thread_join(threads[i]);
    }
    return EXIT_SUCCESS;
}

与 C 版本相比,C++ 版本的运行速度非常慢。

$ g++ -O3 -g -std=c++11 test.cpp -o cpp-test -pthread
$ gcc -O3 -g -std=c89 test.c -o c-test `pkg-config glib-2.0 --cflags --libs`
$ time ./cpp-test

real    0m1.089s
user    0m2.345s
sys     0m1.637s
$ time ./c-test

real    0m0.024s
user    0m0.067s
sys     0m0.000s

所以我运行了 callgrind 分析器。

对于cpp-test__cxz_throw 被调用了 400,000 次,自身成本为 8,000,032。

对于c-test__longjmp_chk 被调用了 400,000 次,自身成本为 5,600,000。

cpp-test 的总成本是 4,048,441,756。

c-test 的总成本是 60,417,722。


我猜想不仅仅是保存跳转点的状态并稍后恢复是通过 C++ 异常完成的。我无法使用更大的 N 进行测试,因为 callgrind 分析器将永远运行以进行 C++ 测试。

至少在这个例子中,C++ 异常导致它比 setjmp/longjmp 对慢很多倍的额外成本是什么?

【问题讨论】:

  • 这里有各种时髦的信息:stackoverflow.com/questions/13835817/…
  • 您正在测试应该经常调用的异常的性能。如果你的程序抛出了很多导致性能问题的异常,那么你就有了一个严重的问题。您应该测试的是使用 try/catch 块与不使用 try/catch 的对比,看看让异常准备好来处理问题的成本。这将向您展示在真正需要时准备好异常的成本。
  • 异常(正确完成)与 setjmp/longjmp 有很大不同。除了例外情况,您实际上可以处理问题并保持程序运行。
  • 您需要提供有关您的 g++ 构建的更多信息。在 Windows 中,g++ 可以使用 3 种不同的异常处理机制构建:SetJmp/LongJmp、Win32 SEH 或 Dwarf2。后两个选项是(我相信)零开销:如果没有抛出异常,那么就没有运行时惩罚。当您输入try { 块时,SJLJ 会浪费时间设置 setjmp。我不确定,但我怀疑你会发现如果你使用 SJLJ,那么你会得到与你的 C 程序相似的结果; SEH 和 Dwarf2 选项使其在最常见的用例中更快(即没有抛出任何东西)
  • 请注意,异常涉及setjmp/longjmp 完全忽略的各种清理工作。如果您在调用setjmp 和调用longjmp 之间分配了内存,那么您在调用longjmp 时可能已经泄漏了它。很可能您也泄露了其他资源(文件描述符等)。即使不在您的测试代码中,在一般情况下也是如此。是的,setjmplongjmp 有效,但它们通常是一种非常粗糙的异常处理机制。

标签: c++ c exception setjmp


【解决方案1】:

这是设计使然。

C++ 异常在本质上应该是exceptional 并因此被优化。当没有发生异常时,程序被编译为最有效的。

您可以通过注释掉测试中的异常来验证这一点。

在 C++ 中:

    //throw 1;

$ g++ -O3 -g -std=c++11 test.cpp -o cpp-test -pthread

$ time ./cpp-test

real    0m0.003s
user    0m0.004s
sys     0m0.000s

在 C 中:

    /*longjmp(*pjb, 1);*/

$ gcc -O3 -g -std=c89 test.c -o c-test `pkg-config glib-2.0 --cflags --libs`

$ time ./c-test

real    0m0.008s
user    0m0.012s
sys     0m0.004s

至少在这个例子中,C++ 异常导致它比 setjmp/longjmp 对慢很多倍的额外成本是什么?

g++ 实现了零成本模型异常,当异常不抛出时,它没有有效的开销*。机器码的生成就像没有 try/catch 块一样。

这种零开销的成本是,当异常被抛出时,必须在程序计数器上执行表查找,以确定跳转到适当的代码用于执行堆栈展开。这会将整个 try/catch 块实现放在执行 throw 的代码中。

您的额外费用是查表。

*可能会出现一些次要的时序巫术,因为 PC 查找表的存在可能会影响内存布局,这可能会影响 CPU 缓存未命中。

【讨论】:

  • "您的额外费用是查表。"轻描淡写。在热表中查找表(在这段代码中,表肯定是热的,因为从同一行一遍又一遍地抛出相同的异常)将花费很少。 setjmp/longjmp 避免的一大成本是内存分配;每个抛出的异常都涉及动态内存分配(例如,在g++ 上,它调用__cxa_allocate_exception,必须稍后释放),然后throw 本身涉及组装大量堆栈展开信息(其中大部分被丢弃而不被在这种情况下使用)在跳转到处理程序代码之前。
猜你喜欢
  • 1970-01-01
  • 2011-06-25
  • 1970-01-01
  • 2020-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多