C++ / Exceptions и производительность

в 18:45, , рубрики: c plus plus, exception, performance, метки: , ,

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

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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js