влияние на производительность глубокого дерева наследования в c ++ - PullRequest
8 голосов
/ 05 ноября 2011

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

Ответы [ 3 ]

13 голосов
/ 05 ноября 2011

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

Строительство / уничтожение

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

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

Размер объекта

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

Вызовы функций-членов, статические

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

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

Вызовы функций-членов, динамические

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

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

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

Pointer Casts, Static

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

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

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

Указатель Приведения, Динамический

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

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

TYPEOF

Это означает использование оператора typeof для извлечения объекта std::type_info, связанного с объектом.

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

Заключение

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

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

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

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

Убедитесь, что ваш дизайн не противоречит одному из этих принципов.

2 голосов
/ 05 ноября 2011

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

0 голосов
/ 05 ноября 2011

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...