Наследование само по себе бесплатно.Например, ниже, B
и C
одинаковы с точки зрения производительности / памяти:
struct A { int x; };
struct B : A { int y; };
struct C { int x, y; };
Наследование требует затрат только при наличии виртуальных функций.
struct A { virtual ~A(); };
struct B : A { ... };
Здесь практически во всех реализациях и A
, и B
будут на один размер указателя больше из-за виртуальной функции.
Виртуальные функции также имеют другие недостатки (по сравнению с не виртуальными функциями)
- Виртуальные функции требуют, чтобы вы вызывали vtable при вызове.Если этот vtable отсутствует в кеше, вы получите промах L2, который может быть невероятно дорогим для встраиваемых платформ (например, более 600 циклов на игровых консолях текущего поколения).
- Даже если вы попадете в кэш L2, если вы переходите к множеству различных реализаций, вы, скорее всего, получите ошибочное прогнозирование переходов для большинства вызовов, что приведет к сбросу конвейера, что опять-таки будет стоить много циклов.практически невозможно встроить (за исключением редких случаев).Если вызываемая функция невелика, это может привести к серьезному снижению производительности по сравнению с встроенной не виртуальной функцией.
- Виртуальные вызовы могут способствовать раздуванию кода.Каждый вызов виртуальной функции добавляет несколько байтов инструкций для поиска виртуальной таблицы и много байтов для самой виртуальной таблицы.
Если вы используете множественное наследование, тогда все становится хуже.
Часто людискажет вам «не беспокойтесь о производительности, пока ваш профилировщик не скажет вам», но это ужасный совет, если производительность вообще важна для вас.Если вы не беспокоитесь о производительности, то происходит то, что вы получаете виртуальные функции повсюду, и когда вы запускаете профилировщик, нет ни одной горячей точки, которая нуждается в оптимизации - вся база кода нуждается в оптимизации.
Мой совет - проектировать для производительности, если это важно для вас.Дизайн, чтобы избежать необходимости виртуальных функций, если это вообще возможно.Проектируйте свои данные вокруг кеша: предпочитайте массивы структурам данных на основе узлов, таким как std::list
и std::map
.Даже если у вас есть контейнер из нескольких тысяч элементов с частыми вставками в середину, я бы по-прежнему использовал массив для определенных архитектур.Несколько тысяч циклов, которые вы теряете при копировании данных для вставок, вполне могут быть компенсированы местоположением кэша, которого вы достигнете при каждом обходе (помните стоимость одного L2 промаха кэша? Вы можете ожидать много таких, когдаобход связанного списка)