Решил выложить небольшое исследование на тему того, как влияет поддержка исключений С++ на общую производительность кода.
Мой опыт работы включает в себя несколько лет разработки под разные встроенные системы, где производительность постоянно приходится учитывать при написании кода (системы реального времени, обрабатывающие большой объём информации — скорости процессора и памяти там никогда не бывало «много»). Соответственно, в этой среде программисты обычно достаточно хорошо представляют себе, какие накладные расходы несёт (или не несёт) та или иная возможность, предоставляемая языком С++. К примеру, поддержка 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, это тема отдельной статьи.
Автор: qehgt