C++ 3d.Комментарии

       

Эффективность и структура


За исключением операторов new, delete, type_id, dynamic_cast, throw и блока try, отдельные выражения и инструкции C++ не требуют поддержки во время выполнения.

Хотелось бы отметить, что есть еще несколько очень важных мест, где мы имеем неожиданную и порой весьма существенную "поддержку времени выполнения". Это конструкторы/деструкторы (сложных) объектов, код создания/уничтожения массивов объектов, пролог/эпилог создающих объекты функций и, отчасти, вызовы виртуальных функций.

Для демонстрации данной печальной особенности рассмотрим следующую программу (замечу, что в исходном коде текст программы, как правило, разнесен по нескольким файлам для предотвращения агрессивного выбрасывания "мертвого кода" качественными оптимизаторами): #include <stdio.h> #include <stdlib.h> #include <time.h>

struct A { A(); ~A(); };

void ACon(); void ADes();

void f1() { A a; }

void f2() { ACon(); ADes(); }

long Var, Count;

A::A() { Var++; } A::~A() { Var++; }

void ACon() { Var++; } void ADes() { Var++; }

int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]);

clock_t c1,c2; { c1=clock();

for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f1();



c2=clock(); printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } { c1=clock();

for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f2();

c2=clock(); printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } }

В ней функции f1() и f2() делают одно и то же, только первая неявно, с помощью конструктора и деструктора класса A, а вторая с помощью явного вызова ACon() и ADes().

Для работы программа требует одного параметра -- сколько миллионов раз вызывать тестовые функции. Выберите значение, позволяющее f1() работать несколько секунд и посмотрите на результат для f2().

При использовании качественного оптимизатора никакой разницы быть не должно; тем не менее, на некоторых платформах она определенно есть и порой достигает 10 раз!


А что же inline? Давайте внесем очевидные изменения: struct A { A() { Var++; }

~A() { Var++; }

};

void f1() { A a; }

void f2() { Var++; Var++;

}

Теперь разницы во времени работы f1() и f2() не быть должно. К несчастью, на большинстве компиляторов она все же присутствует.

Что же происходит? Наблюдаемый нами эффект называется abstraction penalty, т.е. обратная сторона абстракции или налагаемое на нас некачественными компиляторами наказание за использование (объектно-ориентированных) абстракций.

Давайте посмотрим как abstraction penalty проявляется в нашем случае.

Что же из себя представляет void f1() { A a; }

эквивалентное void f1() // псевдокод { A::A(); A::~A(); }

И чем оно отличается от простого вызова двух функций: void f2() { ACon(); ADes(); }

В данном случае -- ничем! Но, давайте рассмотрим похожий пример: void f1() { A a; f(); }

void f2() { ACon(); f(); ADes(); }

Как вы думаете, эквивалентны ли данные функции? Правильный ответ -- нет, т.к. f1() представляет собой void f1() // псевдокод { A::A();

try { f(); } catch (...) { A::~A(); throw; }

A::~A(); }

Т.е. если конструктор успешно завершил свою работу, то языком гарантируется, что обязательно будет вызван деструктор. Т.е. там, где создаются некоторые объекты, компилятор специально вставляет блоки обработки исключений для гарантии вызова соответствующих деструкторов. А накладные расходы в оригинальной f1() чаще всего будут вызваны присутствием ненужных в данном случае блоков обработки исключений (фактически, присутствием "утяжеленных" прологов/эпилогов): void f1() // псевдокод { A::A();

try { // пусто } catch (...) { A::~A(); throw; }

A::~A(); }

Дело в том, что компилятор обязан корректно обрабатывать все возможные случаи, поэтому для упрощения компилятора его разработчики часто не принимают во внимание "частные случаи", в которых можно не генерировать ненужный код. Увы, подобного рода упрощения компилятора очень плохо сказываются на производительности интенсивно использующего средства абстракции и inline функции кода. Хорошим примером подобного рода кода является STL, чье использование, при наличии плохого оптимизатора, вызывает чрезмерные накладные расходы.

Поэкспериментируйте со своим компилятором для определения его abstraction penalty -- гарантированно пригодится при оптимизации "узких мест".


Содержание раздела