Наличие хотя бы одного виртуального метода в классе C ++ (или любом из его родительских классов) означает, что класс будет иметь виртуальную таблицу, и каждый экземпляр будет иметь виртуальный указатель.
Так что стоимость памяти вполне понятна. Наиболее важным является стоимость памяти для экземпляров (особенно, если экземпляры небольшие, например, если они предназначены только для целого числа: в этом случае наличие виртуального указателя в каждом экземпляре может удвоить размер экземпляров. пространство памяти, используемое виртуальными таблицами, я полагаю, обычно незначительно по сравнению с пространством, используемым фактическим кодом метода.
Это подводит меня к моему вопросу: есть ли измеримые затраты на производительность (т.е. влияние на скорость) для создания виртуального метода? Поиск в виртуальной таблице будет производиться во время выполнения при каждом вызове метода, поэтому, если есть очень частые вызовы этого метода и если этот метод очень короткий, то может быть ощутимое снижение производительности? Я думаю, это зависит от платформы, но кто-нибудь запускал тесты?
Причина, по которой я спрашиваю, заключается в том, что я обнаружил ошибку, которая произошла из-за того, что программист забыл определить виртуальный метод. Я не впервые вижу такую ошибку. И я подумал: почему мы добавляем ключевое слово virtual, когда это необходимо, вместо того, чтобы удалять ключевое слово virtual, когда мы абсолютно уверены, что оно не нужно? Если стоимость производительности низкая, я думаю, что я просто порекомендую следующее в своей команде: просто сделайте каждый метод виртуальным по умолчанию, включая деструктор, в каждом классе, и удаляйте его только тогда, когда вам нужно. Вам это кажется безумным?
источник
Ответы:
Я провел несколько таймингов на процессоре PowerPC с частотой 3 ГГц. В этой архитектуре вызов виртуальной функции стоит на 7 наносекунд больше, чем прямой (не виртуальный) вызов функции.
Так что не стоит беспокоиться о стоимости, если только функция не является чем-то вроде тривиального метода доступа Get () / Set (), в котором все, кроме встроенного, является расточительным. Накладные расходы в 7 нс на функцию, встроенную в 0,5 нс, являются серьезными; накладные расходы в 7 нс на функцию, выполнение которой занимает 500 мс, бессмысленны.
Большая стоимость виртуальных функций на самом деле заключается не в поиске указателя функции в vtable (обычно это всего лишь один цикл), а в том, что косвенный переход обычно не может быть предсказан ветвлением. Это может вызвать большой пузырь конвейера, поскольку процессор не может получить какие-либо инструкции, пока косвенный переход (вызов через указатель функции) не прекратится и не будет вычислен новый указатель инструкции. Итак, стоимость вызова виртуальной функции намного больше, чем может показаться при взгляде на сборку ... но все же всего 7 наносекунд.
Изменить: Эндрю, Not Sure и другие также поднимают очень хороший вопрос о том, что вызов виртуальной функции может вызвать промах в кеше инструкций: если вы перейдете на кодовый адрес, которого нет в кеше, вся программа остановится, пока инструкции берутся из основной памяти. Это всегда значительное стойло: на ксенон, около 650 циклов (по моим тестам).
Однако это не проблема виртуальных функций, потому что даже прямой вызов функции вызовет промах, если вы перейдете к инструкциям, которых нет в кеше. Важно то, выполнялась ли функция раньше (что увеличивает вероятность того, что она находится в кеше), и может ли ваша архитектура предсказать статические (не виртуальные) ветки и заблаговременно загрузить эти инструкции в кеш. Мой PPC не работает, но, возможно, самое последнее оборудование Intel делает.
Мои тайминги контролируют влияние промахов icache на выполнение (намеренно, поскольку я пытался изолированно исследовать конвейер ЦП), поэтому они скидывают эту стоимость.
источник
При вызове виртуальной функции определенно существуют измеримые накладные расходы - вызов должен использовать vtable для разрешения адреса функции для этого типа объекта. Дополнительные инструкции - наименьшее из ваших беспокойств. Vtables не только предотвращает многие потенциальные оптимизации компилятора (поскольку тип является полиморфным компилятору), они также могут разрушить ваш I-Cache.
Конечно, будут ли эти штрафы значительными или нет, зависит от вашего приложения, от того, как часто выполняются эти пути кода, и от ваших шаблонов наследования.
Однако, на мой взгляд, использование виртуального устройства по умолчанию - это общее решение проблемы, которую вы могли бы решить другими способами.
Возможно, вы могли бы посмотреть, как создаются / документируются / пишутся классы. Как правило, заголовок класса должен четко указывать, какие функции могут быть переопределены производными классами и как они вызываются. Если программисты напишут эту документацию, это поможет убедиться, что они правильно помечены как виртуальные.
Я бы также сказал, что объявление каждой функции виртуальной может привести к большему количеству ошибок, чем просто забыть пометить что-то как виртуальное. Если все функции виртуальные, все можно заменить базовыми классами - общедоступными, защищенными, частными - все становится честной игрой. Случайно или намеренно подклассы могут затем изменить поведение функций, которые затем вызывают проблемы при использовании в базовой реализации.
источник
save
которая полагается на конкретную реализацию функцииwrite
в базовом классе, то мне кажется, что она либоsave
плохо закодирована, либоwrite
должна быть частной.Это зависит. :) (А вы чего-нибудь еще ждали?)
Как только класс получает виртуальную функцию, он больше не может быть типом данных POD (возможно, его и раньше не было, и в этом случае это не будет иметь значения), и это делает невозможным целый ряд оптимизаций.
std :: copy () для простых типов POD может прибегать к простой процедуре memcpy, но с типами, не относящимися к POD, нужно обращаться более осторожно.
Создание становится намного медленнее, потому что необходимо инициализировать vtable. В худшем случае разница в производительности между типами данных POD и не-POD может быть значительной.
В худшем случае вы можете увидеть в 5 раз более медленное выполнение (это число взято из университетского проекта, который я недавно реализовал, чтобы переопределить несколько стандартных библиотечных классов. Нашему контейнеру потребовалось примерно в 5 раз больше времени для создания, как только тип данных, который он хранит, получил vtable)
Конечно, в большинстве случаев вы вряд ли увидите какую-либо измеримую разницу в производительности, это просто указывает на то, что в некоторых пограничных случаях это может быть дорогостоящим.
Однако производительность не должна быть здесь вашим главным приоритетом. Сделать все виртуальным - не лучшее решение по другим причинам.
Разрешение переопределения всего в производных классах значительно усложняет поддержание инвариантов классов. Каким образом класс гарантирует, что он остается в согласованном состоянии, если любой из его методов может быть переопределен в любое время?
Сделав все виртуальным, можно устранить несколько потенциальных ошибок, но при этом появятся новые.
источник
Если вам нужна функциональность виртуальной диспетчеризации, вы должны заплатить цену. Преимущество C ++ заключается в том, что вы можете использовать очень эффективную реализацию виртуальной диспетчеризации, предоставляемую компилятором, вместо возможно неэффективной версии, которую вы реализуете самостоятельно.
Однако возиться с накладными расходами, если они вам не нужны, возможно, зайдет слишком далеко. И большинство классов не предназначены для наследования - для создания хорошего базового класса требуется больше, чем просто сделать его функции виртуальными.
источник
Виртуальная отправка на порядок медленнее, чем некоторые альтернативы - не столько из-за косвенного обращения, сколько из-за предотвращения встраивания. Ниже я проиллюстрирую это, сравнивая виртуальную диспетчеризацию с реализацией, встраивающей «тип (-идентифицирующее) число» в объекты и используя оператор switch для выбора кода, зависящего от типа. Это полностью исключает накладные расходы на вызов функции - просто выполняется локальный переход. Есть потенциальные затраты на ремонтопригодность, зависимости перекомпиляции и т. Д. Из-за принудительной локализации (в переключателе) специфичных для типа функций.
РЕАЛИЗАЦИЯ
РЕЗУЛЬТАТЫ ДЕЯТЕЛЬНОСТИ
В моей системе Linux:
Это говорит о том, что встроенный подход с переключением типов примерно в (1,28 - 0,23) / (0,344 - 0,23) = 9,2 раза быстрее. Конечно, это специфично для конкретной тестируемой системы / флагов компилятора, версии и т. Д., Но в целом показательно.
КОММЕНТАРИИ RE VIRTUAL DISPATCH
Однако следует сказать, что накладные расходы на вызов виртуальных функций редко бывают значительными, и то только для часто называемых тривиальных функций (таких как геттеры и сеттеры). Даже в этом случае вы можете предоставить единую функцию для получения и настройки сразу множества вещей, минимизируя затраты. Люди слишком сильно беспокоятся о виртуальной диспетчеризации - поэтому сделайте профилирование, прежде чем искать неудобные альтернативы. Основная проблема с ними заключается в том, что они выполняют внешний вызов функции, хотя они также делают локализацию выполняемого кода, который изменяет шаблоны использования кеша (в лучшую или (чаще) худшую сторону).
источник
g++
/clang
и-lrt
. Я подумал, что об этом стоит упомянуть для будущих читателей.Дополнительные затраты практически равны нулю в большинстве сценариев. (простите за каламбур). ejac уже опубликовал разумные относительные меры.
Самое главное, от чего вы отказываетесь, - это возможные оптимизации за счет встраивания. Они могут быть особенно хороши, если функция вызывается с постоянными параметрами. Это редко имеет реальное значение, но в некоторых случаях может быть огромным.
Относительно оптимизации:
важно знать и учитывать относительную стоимость конструкций вашего языка. Нотация Big O - это только половина дела - как масштабируется ваше приложение . Другая половина - это постоянный фактор перед ним.
Как правило, я бы не стал изо всех сил избегать виртуальных функций, если нет четких и конкретных указаний на то, что это узкое место. Чистый дизайн всегда приходит первым - но это только один заинтересованных сторон , которые не должны чрезмерно вредить другим.
Надуманный пример: пустой виртуальный деструктор в массиве из миллиона маленьких элементов может обработать не менее 4 МБ данных, что приведет к потере вашего кеша. Если этот деструктор может быть встроен, данные не будут затронуты.
При написании библиотечного кода такие соображения далеко не преждевременны. Вы никогда не знаете, сколько циклов будет окружено вашей функцией.
источник
Хотя все остальные правы в отношении производительности виртуальных методов и тому подобного, я думаю, что реальная проблема заключается в том, знает ли команда об определении виртуального ключевого слова в C ++.
Рассмотрим этот код, каков результат?
Здесь ничего удивительного:
Поскольку нет ничего виртуального. Если ключевое слово virtual добавлено перед Foo в классах A и B, мы получим это для вывода:
Практически то, чего все ожидают.
Вы упомянули, что есть ошибки, потому что кто-то забыл добавить виртуальное ключевое слово. Итак, рассмотрим этот код (где ключевое слово virtual добавлено к классу A, но не к классу B). Что же тогда на выходе?
Ответ: Так же, как если бы виртуальное ключевое слово было добавлено к B? Причина в том, что подпись для B :: Foo в точности совпадает с A :: Foo () и поскольку Foo для A является виртуальным, то же самое и для B.
Теперь рассмотрим случай, когда Foo B виртуально, а A - нет. Что же тогда на выходе? В этом случае на выходе будет
Ключевое слово virtual работает вниз по иерархии, а не вверх. Он никогда не делает виртуальными методы базового класса. Первый раз виртуальный метод встречается в иерархии, когда начинается полиморфизм. У более поздних классов нет возможности сделать предыдущие классы виртуальными.
Не забывайте, что виртуальные методы означают, что этот класс дает будущим классам возможность переопределять / изменять некоторые из его поведения.
Поэтому, если у вас есть правило для удаления виртуального ключевого слова, оно может не дать желаемого эффекта.
Ключевое слово virtual в C ++ - мощная концепция. Вы должны убедиться, что каждый член команды действительно знает эту концепцию, чтобы ее можно было использовать так, как задумано.
источник
В зависимости от вашей платформы накладные расходы на виртуальный вызов могут быть очень нежелательными. Объявляя каждую функцию виртуальной, вы, по сути, вызываете их все через указатель функции. По крайней мере, это дополнительное разыменование, но на некоторых платформах PPC для этого будут использоваться микрокодированные или иным образом медленные инструкции.
Я бы не рекомендовал ваше предложение по этой причине, но если оно поможет вам предотвратить ошибки, возможно, стоит пойти на компромисс. Я не могу не думать, что должна быть какая-то золотая середина, которую стоит найти.
источник
Для вызова виртуального метода потребуется всего пара дополнительных инструкций asm.
Но я не думаю, что вы беспокоитесь о том, что fun (int a, int b) имеет пару дополнительных инструкций push по сравнению с fun (). Так что не беспокойтесь о виртуальных машинах, пока вы не окажетесь в особой ситуации и не увидите, что это действительно приводит к проблемам.
PS Если у вас есть виртуальный метод, убедитесь, что у вас есть виртуальный деструктор. Так вы избежите возможных проблем
В ответ на комментарии «xtofl» и «Tom». Я провел небольшие тесты с 3 функциями:
Мой тест был простой итерацией:
И вот результаты:
Он был скомпилирован VC ++ в режиме отладки. Я провел только 5 тестов на метод и вычислил среднее значение (так что результаты могут быть довольно неточными) ... В любом случае, значения почти равны при 100 миллионах вызовов. И метод с 3 дополнительными толчками / толчками был медленнее.
Главное, что если вам не нравится аналогия с push / pop, подумайте о дополнительных if / else в вашем коде? Вы думаете о конвейере ЦП, когда добавляете дополнительные if / else ;-) Кроме того, вы никогда не знаете, на каком ЦП будет выполняться код ... Обычный компилятор может генерировать код, более оптимальный для одного процессора и менее оптимальный для другого ( Intel Компилятор C ++ )
источник
final
в своем переопределении и у вас есть указатель на производный тип, а не на базовый тип ). В этом тесте каждый раз вызывалась одна и та же виртуальная функция, поэтому он идеально предсказывал; отсутствие пузырей в трубопроводе, кроме случаев ограниченнойcall
пропускной способности. И это косвенноеcall
может быть еще парой упов. Прогнозирование ветвлений хорошо работает даже для непрямых ветвей, особенно если они всегда в одном месте.call
чем для прямогоcall
. (И да, обычныеcall
инструкции тоже нуждаются в прогнозировании. Этап выборки должен знать следующий адрес для выборки до того, как этот блок будет декодирован, поэтому он должен прогнозировать следующий блок выборки на основе адреса текущего блока, а не адреса инструкции. как предсказать, где в этом блоке есть инструкция ветвления ...)