последствия глубокого дерева наследования для производительности в c++

есть ли какой-либо недостаток эффективности, связанный с глубокими деревьями наследования (в c++), i.e, большой набор классов A, B, C и т. д., такой, что B расширяет A, C расширяет B и т. д. Одно из последствий эффективности, о котором я могу думать, заключается в том, что когда мы создаем экземпляр самого нижнего класса, скажем C, тогда также называются конструкторы B и A, что будет иметь последствия для производительности.

3 ответов


перечислим операции, которые мы должны рассмотреть:

строительство/разрушение

каждый конструктор / деструктор будет вызывать свои эквиваленты базового класса. Однако, как заметил Джеймс Макнеллис, вы все равно собирались это сделать. Вы не производили от A только потому, что он был там. Так что работа будет сделана так или иначе.

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

Объект В Размере

в общем случае накладные расходы для производного класса-ничто. Накладные расходы для виртуальных членов-это указатель или виртуальное наследование.

член Вызовы Функций, Static

под этим я подразумеваю вызов не виртуальных функций-членов или вызов виртуальных функций-членов с именами классов (синтаксис ClassName::FunctionName). Оба они позволяют компилятору знать во время компиляции, какую функцию вызывать.

производительность этого инвариантна к размеру иерархии, так как определяется время компиляции.

Вызовы Функций-Членов, Dynamic

это вызов виртуальных функций с полной и полное ожидание вызовов среды выполнения.

в большинстве здравых реализаций C++ это инвариантно к размеру иерархии объектов. Большинство реализаций используют v-таблицу для каждого класса. Каждый объект имеет указатель v-таблицы в качестве члена. Для любого конкретного динамического вызова компилятор обращается к указателю v-таблицы, выбирает метод и вызывает его. Поскольку v-таблица одинакова для каждого класса, она не будет медленнее для класса с глубокой иерархией, чем для класса с мелкой один.

виртуальное наследование играет немного с этим.

Наведение Указателя, Static

это относится к static_cast или любая эквивалентная операция. Это означает неявное приведение из производного класса в базовый класс, явное использование static_cast или C-стиля бросает, и т. д.

обратите внимание, что это технически включает в себя справочные литья.

производительность статических приведений между классами (вверх или вниз) инвариантна размеру иерархии. Любой смещения указателей будут создаваться во время компиляции. Это должно быть верно для виртуального наследования, а также для не виртуального наследования, но я не уверен на 100%.

Указатель Бросает, Динамический

это, очевидно, относится к явному использованию dynamic_cast. Это обычно используется при приведении из базового класса в производный.

производительность dynamic_cast вероятно, изменится для большой иерархии. Но разумные реализации должны проверять только классы между текущий класс и запрошенный. Таким образом, он просто линейный по числу классов между ними, а не линейный по числу классов в иерархии.

typeof на

это означает использование typeof для получения std::type_info объект, связанный с объектом.

производительность этого будет инвариантна размеру иерархии. Если класс является виртуальным (имеет виртуальные функции или виртуальные базовые классы), то он просто вытащит его из vtable. Если он не является виртуальным, то он определяется временем компиляции.

вывод

короче говоря, большинство операций инвариантны к размеру иерархии. Но даже в тех случаях, когда это имеет влияние, это не проблема.

Я был бы больше обеспокоен некоторой этикой дизайна, где вы чувствовали необходимость построить такую иерархию. По моему опыту, иерархии, подобные этой, происходят из двух линий дизайна.

  1. идеал Java/C# имея все производные от общего базового класса. Это ужасная идея в C++ и никогда не должны использоваться. Каждый объект должен выводиться из того, что он должен to, и только это. C++ был построен на принципе" платить за то, что вы используете", и вывод из общей базы работает против этого. В общем, все, что вы можете сделать с таким общим базовым классом, - это либо то, что вы не должны делать period, либо то, что можно сделать с перегрузкой функций (используя operator<< конвертировать к строкам, например).

  2. злоупотребление наследством. Использование наследования, когда вы должны использовать сдерживание. Наследование создает отношение" есть " между объектами. Чаще всего отношения" имеет " (один объект имеет другого в качестве члена) гораздо более полезны и гибки. Они облегчают скрытие данных, и вы не позволяете пользователю притворяться, что один класс является другим.

убеждайтесь что ваша конструкция не падает afoul один из этих принципов.


будет, но не так плохо, как программист влияет на производительность.


как указывает @Nicol, он может делать несколько вещей. Если это то, что вам нужно сделать, независимо от дизайна, потому что все это именно необходимые шаги в получении программы от call main до exit в течение наименьшего количества возможных циклов ваш дизайн-это просто вопрос ясности кодирования (или, возможно, его отсутствия :).

по моему опыту настройка производительности, как в этом примере, что я часто вижу, как огромный источник потерянном времени чрезмерное проектирование структур данных (т. е. классов). Как ни странно, обоснование структур данных часто (угадайте, что?) - производительность!

по моему опыту, дело в структуре данных-сохранить ее как можно более простой и максимально нормализованной. Если он полностью нормализован, то любое единичное изменение не может сделать его непоследовательным. Не всегда можно добиться полной нормальности, и в этом случае вам придется иметь дело с возможностью того, что данные могут временно несовместимы. Вот почему люди пишут обработчики уведомлений, и это поощряется в ООП. Идея в том, что если вы измените что-то в одном месте, это может вызвать уведомления, которые "автоматически" распространяют изменение в другие места, пытаясь сохранить согласованность.

проблема с уведомлениями, они могут убежать. Просто изменение некоторого логического свойства из true to false может вызвать огненный шторм уведомлений, разрывающих данные структура способами, которые не понимает ни один программист, обновление баз данных, покраска окон,архивация файлов и т. д. так далее. Я часто нахожу, что именно здесь происходит большинство тактов.

Я думаю, что проще и гораздо эффективнее временно терпеть непоследовательность и периодически ремонтировать ее с помощью какого-то широкого процесса.

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