c++ - C++ 异常和 setjmp/longjmp 的代价

标签 c++ c exception setjmp

我编写了一个测试来测量线程中 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 对慢很多倍的额外成本是什么?

最佳答案

这是设计使然。

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

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

在 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

What is the extra cost involved in C++ exceptions making it many times slower than the setjmp/longjmp pair at least in this example?

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

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

您的额外费用是查表。

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

关于c++ - C++ 异常和 setjmp/longjmp 的代价,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31486907/

相关文章:

c - 在C中逐行读取文件,

c - STM32发现F3 SPI环回RXFIFO没有接收到数据

java - 查找是否因为重复项而抛出 SQLException

Java:从 super 构造函数捕获异常

python - 为子进程提供 CPU 时间和内存

c++ - 如何编辑格式设置以增加换行长度?

c++ - 无法使用 QProcess 启动 g++

c++ - 在opengl中用线程绘制顶点

c - 在递归函数中返回和使用多个树结构

c++ - 从 linux 系统错误中抛出 C++ 异常