C ++异常和setjmp / longjmp的成本

22

我编写了一个测试来测量使用线程的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进行测试,因为对于C++测试来说,callgrind分析器将永远运行下去。

C++异常处理中涉及的额外成本是什么,使其在这个例子中比setjmp/longjmp组合慢多倍?


1
这里有各种酷炫的信息:https://dev59.com/K2Yr5IYBdhLWcg3wQH-- - user4581301
5
你正在测试不应该经常调用的异常性能。如果你的程序抛出了很多异常,导致性能问题,那么你有一个严重的问题。你应该测试使用try/catch块和不使用try/catch块的情况,并查看准备处理问题时使用异常的代价。这将向你展示准备好异常以应对真正需要它们时的代价。 - Captain Obvlious
1
异常处理(正确地执行)与setjmp / longjmp 有很大的区别。使用异常处理,您实际上可以处理问题并保持程序运行。 - Hot Licks
1
你需要提供更多关于你的g++构建的信息。在Windows中,g++可以使用三种不同的异常处理机制进行构建:SetJmp/LongJmp、Win32 SEH或Dwarf2。后两个选项(我认为)零开销:如果没有抛出异常,则没有运行时惩罚。SJLJ浪费时间在进入try {块时设置setjmp。我不确定但我认为如果你正在使用SJLJ,那么你会得到类似于C程序的结果;SEH和Dwarf2选项使得对于最常见的使用情况(即没有抛出异常)更快。 - M.M
2
请注意,异常涉及各种清理工作,而 setjmp/longjmp 完全忽略了这些工作。如果在调用 setjmplongjmp 之间分配了内存,则在调用 longjmp 时可能会泄漏该内存。很可能您还泄漏了其他资源(文件描述符等)。即使不是在测试代码中,在一般情况下也是如此。是的,setjmplongjmp 能够工作,但它们通常是一种非常简单的异常处理机制。 - Jonathan Leffler
显示剩余3条评论
1个回答

23

这是设计上的考虑。

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
在这个例子中,C++异常涉及的额外成本是什么,使其比setjmp/longjmp对慢多次?
g++实现了零成本模型异常,当未抛出异常时,它们没有任何有效的开销。生成的机器代码就像没有try/catch块一样。
这种零开销的成本是,当抛出异常时,必须在程序计数器上执行表查找,以确定跳转到适当的代码来执行堆栈展开。这将整个try/catch块实现放置在执行throw的代码内部。
你需要承担的额外成本就是表查找。
*可能会发生一些微小的时间魔法,因为PC查找表的存在可能会影响内存布局,进而影响CPU缓存未命中。

1
“你的额外成本是一个表查找。”这种说法低估了问题。在热表中进行表查找(在此代码中,该表肯定是热的,因为相同的异常一遍又一遍地从同一行抛出)将花费很少。setjmp/longjmp 的一个重要优点是避免内存分配;每个抛出的异常都涉及动态内存分配(例如,在 g++ 上,它调用 __cxa_allocate_exception,稍后必须释放),然后 throw 本身涉及组装大量堆栈展开信息(在这种情况下大部分信息被丢弃而没有使用),然后跳转到处理程序代码。 - ShadowRanger

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