Стоимость исключений С++ и 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.