Pull to refresh

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

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

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

Articles