Стоимость исключений С++ и setjmp/longjmp

Я написал тест, чтобы измерить стоимость исключений С++ с помощью потоков.

#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.

$ 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.


Я думаю, что нечто гораздо большее, чем просто сохранение состояния точки перехода, а затем возобновление выполняется с помощью исключений С++. Я не мог протестировать с более крупным N, потому что профайлер callgrind будет работать вечно для теста С++.

Какова дополнительная стоимость, связанная с исключениями С++, что делает ее во много раз медленнее, чем пара setjmp/longjmp, по крайней мере, в этом примере?

Ответы

Ответ 1

Это по дизайну.

Исключения С++, как ожидается, будут носить исключительный характер и оптимизируются таким образом. Программа скомпилирована как наиболее эффективная, когда исключение не происходит.

Вы можете проверить это, комментируя исключение из своих тестов.

В С++:

    //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

Какова дополнительная стоимость, связанная с исключениями С++, что делает ее во много раз медленнее, чем пара setjmp/longjmp, по крайней мере, в этом примере?

g++ реализует исключения с нулевой стоимостью, которые не имеют эффективных накладных расходов, когда исключение не выбрано. Машинный код создается так, как будто не было блока try/catch.

Стоимость этого нулевого накладного расхода заключается в том, что поиск таблицы должен выполняться на счетчике программы, когда исключение выбрано, чтобы определить переход к соответствующему коду для выполнения разворачивания стека. Это помещает всю реализацию блока try/catch в код, выполняющий throw.

Ваша дополнительная стоимость - это поиск в таблице.

* Может возникнуть некоторая второстепенная синхронизация времени, поскольку наличие таблицы поиска ПК может повлиять на макет памяти, что может повлиять на пропуски кэша CPU.