Виртуальные методы обычно реализуются через так называемые таблицы виртуальных методов (для краткости vtable), в которых хранятся указатели функций. Это добавляет косвенность к фактическому вызову (нужно получить адрес функции для вызова из виртуальной таблицы, затем вызвать ее - вместо того, чтобы просто вызывать ее прямо сейчас). Конечно, это займет некоторое время и еще немного кода.
Однако это не обязательно является основной причиной медлительности. Реальная проблема заключается в том, что компилятор (обычно / обычно) не может знать, какая функция будет вызвана. Поэтому он не может встроить его или выполнить другие подобные оптимизации. Одно это может добавить дюжину бессмысленных инструкций (подготовка регистров, вызов, затем восстановление состояния после этого) и может помешать другим, казалось бы, не связанным оптимизациям. Более того, если вы разветвляетесь как сумасшедший, вызывая много разных реализаций, вы получаете те же хиты, что и ветвление, как от сумасшедшего, с помощью других средств: кеш и предиктор ветвлений вам не помогут, ветки займут больше времени, чем совершенно предсказуемые ветка.
Большое но : эти хиты производительности, как правило, слишком малы, чтобы иметь значение. Им стоит подумать, хотите ли вы создать высокопроизводительный код и добавить виртуальную функцию, которая будет вызываться с пугающей частотой. Однако следует также помнить, что замена вызовов виртуальных функций другими средствами ветвления ( if .. else
, switch
указателями функций и т. Д.) Не решит фундаментальную проблему - она может быть очень медленной. Проблема (если она вообще существует) не в виртуальных функциях, а в (ненужном) косвенном обращении.
Изменить: Разница в инструкциях вызова описана в других ответах. По сути, код для статического («нормального») вызова:
- Скопируйте несколько регистров в стек, чтобы вызываемая функция могла использовать эти регистры.
- Скопируйте аргументы в предопределенные местоположения, чтобы вызываемая функция могла найти их независимо от того, где она вызывается.
- Нажмите обратный адрес.
- Переход / переход к коду функции, который является адресом времени компиляции и, следовательно, жестко закодирован в двоичном виде компилятором / компоновщиком.
- Получить возвращаемое значение из предопределенного местоположения и восстановить регистры, которые мы хотим использовать.
Виртуальный вызов делает то же самое, за исключением того, что адрес функции не известен во время компиляции. Вместо этого пара инструкций ...
- Получите указатель vtable, который указывает на массив указателей на функции (адреса функций), по одному для каждой виртуальной функции, от объекта.
- Получите правильный адрес функции из vtable в регистр (индекс, где хранится правильный адрес функции, определяется во время компиляции).
- Переходите по адресу в этом регистре, а не по жестко закодированному адресу.
Что касается ветвей: Ветвь - это все, что переходит к другой инструкции вместо того, чтобы просто выполнить следующую инструкцию. Это включает в себя if
, switch
, части различных циклов, вызовов функций и т.д. , а иногда компилятор реализует вещи , которые , кажется, не ветви таким образом , что на самом деле нуждается в отделении под капотом. См. Почему обработка отсортированного массива быстрее, чем несортированного массива? почему это может быть медленным, что процессоры делают, чтобы противостоять этому замедлению, и как это не панацея.