Решил выложить небольшое исследование на тему того, как влияет поддержка исключений С++ на общую производительность кода.
Мой опыт работы включает в себя несколько лет разработки под разные встроенные системы, где производительность постоянно приходится учитывать при написании кода (системы реального времени, обрабатывающие большой объём информации — скорости процессора и памяти там никогда не бывало «много»). Соответственно, в этой среде программисты обычно достаточно хорошо представляют себе, какие накладные расходы несёт (или не несёт) та или иная возможность, предоставляемая языком С++. К примеру, поддержка namespace — никаких дополнительных затрат вообще; RTTI — дополнительная секция с именами классов/структур с их type_info (итоговый бинарник увеличивается в размере, но кодогенерацию это не затрагивает); и т.п. Как (не на всех платформах) реализована поддержка исключений мы и посмотрим.
Используемые инструменты: древний front-end от EDG для преобразования кода на C++ в код на С и Artistic Style для форматирования полученных С-файлов (иначе их читать невозможно). Про front-end от EDG надо сказать особо — это именно тот frond-end, который встраивается в компиляторы Intel, компиляторы Texas Instruments и т.п. Так как поддержать все возможности C++ — очень непростая задача (по сравнению с реализацией всех возможностей языка С), то на некоторых платформах происходит трансляция С++ кода в идентичный код на С, а уже этот код «скармливается» С-компилятору. Front-end используется не самый свежий, но для понимания подойдёт.
Итак, возьмём достаточно простой текстовый код (имена и константы выбраны специально такими, чтобы их легко находить в обработанном листинге):
Всё очень просто — два inline конструктора/деструктора, пара виртуальных функций, пара вызовов внешней функции.
Вот получаемый код без поддержки исключений (результат обработан AStyle'ом). Простыня, но это необходимо:
Хорошо видно, как «конструируется» каждый объект, как реализованы vtable/наследование, что конструкторы/деструкторы всё ещё inline, а у C-компилятора всё ещё есть вся информация, чтобы эффективно оптимизировать данный код. Также отметим, что получившийся C-код занимает 72 строки, и примерно 1.6kB.
Теперь этот же исходник оттранслируем с поддержкой исключений.
Результат смотреть здесь: C-эквивалент размером в 253 строки и 8.5 kB. Здесь я это выкладывать не буду, ограничусь основной функцией (бывшей
Главные изменения:
Самая беда именно с первыми двумя пунктами — логика конструкторов/деструкторов усложнилась настолько, что front-end выносит их в отдельные функции. Понятно почему — встраивание их (inline) приведёт к сильному увеличению кода каждой функции, где используются объекты типа BBBB. Но следствием этого станет то, что оптимизатор C-компилятора сгенерирует существенно менее производительный код (у нас появились дополнительные вызовы и проверки в коде).
То есть: просто включение поддержки исключений привело и к увеличению объёма конечного бинарного файла, и к замедлению работы всех функций, внутри которых происходит конструирование объектов (за исключением самых тривиальных).
Собственно, это и есть основная причина, по которой для embedded разработки поддержка исключений по умолчанию выключена — за неё приходится платить, даже если ей реально не пользоваться.
PS: это всё, конечно, не означает, что «исключения — это плохо!» или «используйте коды возврата вместо исключений!». Просто каждый инструмент хорош для своей задачи.
PPS: поддержка обработки ошибочных ситуаций в embedded разработке, конечно же есть и активно используется. Она обычно не использует C++ exceptions, это тема отдельной статьи.
Мой опыт работы включает в себя несколько лет разработки под разные встроенные системы, где производительность постоянно приходится учитывать при написании кода (системы реального времени, обрабатывающие большой объём информации — скорости процессора и памяти там никогда не бывало «много»). Соответственно, в этой среде программисты обычно достаточно хорошо представляют себе, какие накладные расходы несёт (или не несёт) та или иная возможность, предоставляемая языком С++. К примеру, поддержка namespace — никаких дополнительных затрат вообще; RTTI — дополнительная секция с именами классов/структур с их type_info (итоговый бинарник увеличивается в размере, но кодогенерацию это не затрагивает); и т.п. Как (не на всех платформах) реализована поддержка исключений мы и посмотрим.
Используемые инструменты: древний front-end от EDG для преобразования кода на C++ в код на С и Artistic Style для форматирования полученных С-файлов (иначе их читать невозможно). Про front-end от EDG надо сказать особо — это именно тот frond-end, который встраивается в компиляторы Intel, компиляторы Texas Instruments и т.п. Так как поддержать все возможности C++ — очень непростая задача (по сравнению с реализацией всех возможностей языка С), то на некоторых платформах происходит трансляция С++ кода в идентичный код на С, а уже этот код «скармливается» С-компилятору. Front-end используется не самый свежий, но для понимания подойдёт.
Итак, возьмём достаточно простой текстовый код (имена и константы выбраны специально такими, чтобы их легко находить в обработанном листинге):
struct AAAAA {
int a;
virtual void process();
AAAAA() { a = 1234; }
virtual ~AAAAA() {}
};
struct BBBBB : AAAAA {
virtual void process();
BBBBB() { a = 5678; }
virtual ~BBBBB() {}
};
// forward declaration
int bar();
int foo()
{
BBBBB b1;
b1.a = bar();
b1.process();
BBBBB b2;
b2.a = bar();
b2.process();
return b1.a + b2.a;
}
Всё очень просто — два inline конструктора/деструктора, пара виртуальных функций, пара вызовов внешней функции.
Вот получаемый код без поддержки исключений (результат обработан AStyle'ом). Простыня, но это необходимо:
#line 1 "1.cpp"
struct __T9639768;
struct AAAAA;
#line 9
struct BBBBB;
struct __T9639768 {
short d;
short i;
void (*f)();
};
#line 1
struct AAAAA {
int a;
struct __T9639768 *__vptr;
};
#line 9
struct BBBBB {
struct AAAAA __b_AAAAA;
};
#line 17
extern int bar__Fv(void);
extern int foo__Fv(void);
#line 10
extern void process__5BBBBBFv(struct BBBBB *const);
extern struct __T9639768 __vtbl__5AAAAA[3];
extern struct __T9639768 __vtbl__5BBBBB[3];
#line 19
int foo__Fv(void)
{ auto int __T9722792;
auto struct BBBBB b1;
auto struct BBBBB b2;
#line 21
{ {
((b1.__b_AAAAA).__vptr) = __vtbl__5AAAAA;
((b1.__b_AAAAA).a) = 1234;
} ((b1.__b_AAAAA).__vptr) = __vtbl__5BBBBB;
((b1.__b_AAAAA).a) = 5678;
}
((b1.__b_AAAAA).a) = (bar__Fv());
process__5BBBBBFv((&b1));
{ {
((b2.__b_AAAAA).__vptr) = __vtbl__5AAAAA;
((b2.__b_AAAAA).a) = 1234;
} ((b2.__b_AAAAA).__vptr) = __vtbl__5BBBBB;
((b2.__b_AAAAA).a) = 5678;
}
((b2.__b_AAAAA).a) = (bar__Fv());
process__5BBBBBFv((&b2));
{
__T9722792 = ((((b1.__b_AAAAA).a)) + (((b2.__b_AAAAA).a)));
{
((b2.__b_AAAAA).__vptr) = __vtbl__5BBBBB;
{ {
((b2.__b_AAAAA).__vptr) = __vtbl__5AAAAA;
}
}
} {
((b1.__b_AAAAA).__vptr) = __vtbl__5BBBBB;
{ {
((b1.__b_AAAAA).__vptr) = __vtbl__5AAAAA;
}
}
}
return __T9722792;
}
}
Хорошо видно, как «конструируется» каждый объект, как реализованы vtable/наследование, что конструкторы/деструкторы всё ещё inline, а у C-компилятора всё ещё есть вся информация, чтобы эффективно оптимизировать данный код. Также отметим, что получившийся C-код занимает 72 строки, и примерно 1.6kB.
Теперь этот же исходник оттранслируем с поддержкой исключений.
Результат смотреть здесь: C-эквивалент размером в 253 строки и 8.5 kB. Здесь я это выкладывать не буду, ограничусь основной функцией (бывшей
int foo()
) с комментариями некоторых моментов:int foo__Fv(void)
{ static struct __T9641460 __T9653776[2] = {{((void (*)())__dt__5BBBBBFv),((unsigned short)0U),((unsigned short)65535U),((unsigned char)0U)},{((void (*)())__dt__5BBBBBFv),((unsigned short)1U),((unsigned short)0U),((unsigned char)0U)}};
auto void *__T9731464[2];
auto int __T9733536;
auto struct
#line 20
__T9643156 __T9734356;
auto struct BBBBB b1;
auto struct BBBBB b2;
(__T9734356.next) = __curr_eh_stack_entry;
__curr_eh_stack_entry = (&__T9734356);
(__T9734356.kind) = ((unsigned char)1U);
(((__T9734356.variant).function).regions) = ((struct __T9641460 *)__T9653776);
(((__T9734356.variant).function).obj_table) = ((void **)__T9731464);
(((
#line 25
__T9734356.variant).function).saved_region_number) = __eh_curr_region;
__eh_curr_region = ((unsigned short)65535U);
#line 21
__ct__5BBBBBFv((&b1));
(((void **)__T9731464)[0U]) = ((void *)(&b1));
__eh_curr_region = ((unsigned short)0U);
((b1.__b_AAAAA).a) = (bar__Fv());
process__5BBBBBFv((&b1));
__ct__5BBBBBFv((&b2));
(((void **)__T9731464)[1U]) = ((void *)(&b2));
__eh_curr_region = ((unsigned short)1U);
((b2.__b_AAAAA).a) = (bar__Fv());
process__5BBBBBFv((&b2));
{
__T9733536 = ((((b1.__b_AAAAA).a)) + (((b2.__b_AAAAA).a)));
__eh_curr_region = ((unsigned short)0U);
__dt__5BBBBBFv((&b2), 2);
__eh_curr_region = ((unsigned short)65535U);
__dt__5BBBBBFv((&b1), 2);
{
__eh_curr_region = ((((__T9734356.variant).function).saved_region_number));
__curr_eh_stack_entry =
#line 29
((__T9734356.next));
return __T9733536;
}
}
}
Главные изменения:
- конструкторы перестали быть inline (появились вызовы __ct__5BBBBBFv),
- аналогичная ситуация с деструкторами (__dt__5BBBBBFv),
- в коде теперь отслеживается, какой из объектов уже сконструирован (или уже удалён), а какой ещё нет — так как нужно знать, деструкторы каких объектов требуется вызвать, если произойдёт исключение,
- код конструкторов/деструкторов усложнился (функции __dt__5BBBBBFv/ct__5BBBBBFv, смотреть по ссылке),
Самая беда именно с первыми двумя пунктами — логика конструкторов/деструкторов усложнилась настолько, что front-end выносит их в отдельные функции. Понятно почему — встраивание их (inline) приведёт к сильному увеличению кода каждой функции, где используются объекты типа BBBB. Но следствием этого станет то, что оптимизатор C-компилятора сгенерирует существенно менее производительный код (у нас появились дополнительные вызовы и проверки в коде).
То есть: просто включение поддержки исключений привело и к увеличению объёма конечного бинарного файла, и к замедлению работы всех функций, внутри которых происходит конструирование объектов (за исключением самых тривиальных).
Собственно, это и есть основная причина, по которой для embedded разработки поддержка исключений по умолчанию выключена — за неё приходится платить, даже если ей реально не пользоваться.
PS: это всё, конечно, не означает, что «исключения — это плохо!» или «используйте коды возврата вместо исключений!». Просто каждый инструмент хорош для своей задачи.
PPS: поддержка обработки ошибочных ситуаций в embedded разработке, конечно же есть и активно используется. Она обычно не использует C++ exceptions, это тема отдельной статьи.