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

The Cost of C++ Exceptions and setjmp/longjmp

本文关键字:longjmp 代价 setjmp 异常 c++      更新时间:2023-10-16

我写了一个测试来测量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("errorn");
            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被调用40万次,自成本为8,000,032。

对于c-test, __longjmp_chk被调用正好40万次,自成本为560万。

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

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


我猜c++异常比简单地保存跳转点的状态和稍后恢复更重要。我不能用更大的N进行测试,因为callgrind分析器将永远运行c++测试。

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

这是通过设计。

c++异常在本质上应该是exception ,并因此进行了优化。当不发生异常时,程序将被编译为最有效的。

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

在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缓存丢失