Из http://www.boost.org/community/implementation_variations.html
"... кодирование различий, таких как изменение класса от виртуального до невиртуальных участников или удаления уровня абстракции вряд ли будет иметь любое измеримое значение если глубоко во внутреннем цикле. И даже во внутреннем цикле, современные центральные процессоры часто выполняют такие конкурирующие кодовые последовательности в том же количестве тактов!"
Я пытаюсь понять "даже во внутреннем цикле" часть. Конкретно, что механизмы центральные процессоры реализуют для выполнения этих двух кодов (виртуальный по сравнению с невиртуальным или дополнительным уровнем абстракции) в том же количестве тактов? Я знаю о конвейерной обработке инструкции и кэшировании, но как возможно выполнить виртуальный вызов в том же количестве тактов как невиртуальный вызов? Как косвенность "потеряна"?
Кэширование (например, кэширование цели ветвления ), параллельные единицы загрузки (часть конвейерной обработки, но также такие вещи, как «попадание при промахе», которые не останавливают конвейер), и выполнение вне очереди , вероятно, поможет преобразовать нагрузку
- нагрузку
- ветвь
во что-то более близкое к фиксированному филиал
.Сворачивание / исключение инструкций (какой правильный термин для этого?) На этапе декодирования или предсказания ветвления конвейера также может способствовать.
Все это зависит от множества разных вещей: сколько существует различных целей ветвления (например, сколько различных виртуальных перегрузок вы, вероятно, вызовете), сколько вещей вы перебираете (является ли целевой кеш ветвления) теплый »? как насчет icache / dcache?), как виртуальные таблицы или косвенные таблицы размещаются в памяти (удобны ли они для кеширования или каждая новая загрузка vtable может вытеснять старую vtable?), аннулируется ли кеш неоднократно из-за многоядерного пинг-понга и т. д.
(Заявление об ограничении ответственности: я определенно не эксперт, и многие мои знания получены при изучении упорядоченных встроенных процессоров, поэтому некоторые из них являются экстраполяцией. Если у вас есть исправления, не стесняйтесь комментировать!)
Правильный способ определить, будет ли это проблемой для конкретной программы, - это, конечно, профилировать. Если можете, сделайте это с помощью аппаратных счетчиков - они могут многое рассказать вам о том, что происходит на различных этапах конвейера.
Редактировать:
Как указывает Ханс Пассан в вышеприведенном комментарии Оптимизация косвенного обращения к внутреннему циклу современного процессора , ключом к тому, чтобы эти две вещи занимали одинаковое количество времени, является способность эффективно " удалите »более одной инструкции за цикл. Исключение инструкций может помочь в этом, но суперскалярный дизайн , вероятно, более важен (попадание при промахе - очень маленький и конкретный пример, полностью резервированные единицы нагрузки могут быть лучше).
Давайте возьмем идеальную ситуацию и предположим, что прямая ветвь - это всего одна инструкция:
branch dest
... а косвенная ветвь - три (может быть, вы можете получить ее из двух, но она больше одной):
load vtable from this
load dest from vtable
branch dest
Предположим, что это абсолютно идеальная ситуация: * эта и вся vtable находятся в кэше L1, кэш L1 достаточно быстр, чтобы поддерживать амортизированную стоимость одного цикла на инструкцию для двух загрузок. (Вы даже можете предположить, что процессор переупорядочил нагрузки и смешал их с более ранними инструкциями, чтобы дать им время для их завершения до перехода; это не имеет значения для этого примера.) Также предположим, что целевой кеш ветвления является горячим и нет конвейера стоимость очистки для ветви, а инструкция перехода сводится к одному циклу (амортизируется).
Следовательно, теоретическое минимальное время для первого примера составляет 1 цикл (с амортизацией).
Теоретический минимум для второго примера, при отсутствии исключения инструкций или избыточных функциональных модулей или чего-то, что позволит исключить более одной инструкции за цикл, составляет 3 цикла (есть 3 инструкции)!
Непрямая загрузка всегда будет медленнее из-за большего количества инструкций, пока вы не достигнете чего-то вроде суперскалярного дизайна, который позволяет выводить более одной инструкции за цикл.
Как только у вас есть это, минимум для обоих примеров становится где-то между 0 и 1 циклом, опять же, при условии, что все остальное идеально. Возможно, у вас должны быть более идеальные условия для второго примера, чтобы действительно достичь этого теоретического минимума, чем для первого примера, но теперь это возможно.
В некоторых случаях, которые вас волнуют, вы, вероятно, не достигнете этого минимума ни в одном из примеров. Либо целевой кеш ветки будет холодным, либо vtable не будет в кеше данных,или машина не сможет переупорядочить инструкции, чтобы в полной мере использовать резервные функциональные блоки.
... здесь на помощь приходит профилирование, что в любом случае является хорошей идеей.
Вы можете просто поддерживать небольшую паранойю по поводу виртуалов. См. статью Ноэля Ллописа о дизайне, ориентированном на данные , прекрасные слайды Ловушки объектно-ориентированного программирования и сварливые, но образовательные презентации Майка Эктона . Теперь вы внезапно перешли к шаблонам, которые, вероятно, уже устраивают ЦП, если вы обрабатываете много данных.
Функции языка высокого уровня, такие как виртуальный, обычно представляют собой компромисс между выразительностью и контролем. Я честно думаю, однако, что, просто увеличивая вашу осведомленность о том, что на самом деле делает virtual (не бойтесь время от времени читать представление разборки и определенно заглядывать в руководства по архитектуре вашего процессора), вы, скорее всего, будете его использовать. когда это имеет смысл, а не когда нет, а профилировщик может покрыть все остальное, если это необходимо.
Универсальные утверждения о том, что «не используйте виртуальное» или «виртуальное использование вряд ли будет иметь измеримое значение», делают меня ворчливым. Реальность обычно более сложна, и либо вы окажетесь в ситуации, когда вы достаточно заботитесь, чтобы профилировать или избегать этого, либо вы находитесь в тех других 95%, где, вероятно, не стоит заботиться, за исключением возможного образовательного контента.
Конвейер - это основной способ.
Для загрузки инструкции, ее декодирования, выполнения действий и загрузки косвенных ссылок на память может потребоваться 20 тактов. Но из-за конвейерной линии процессор может выполнять части 19 других инструкций одновременно на разных этапах конвейера, обеспечивая общую пропускную способность в 1 инструкцию за каждый тактовый цикл, независимо от того, сколько времени фактически требуется для подачи этой инструкции через конвейер.
Современные процессоры используют технику адаптивного прогнозирования переходов, которая может прогнозировать множество косвенных переходов, таких как vtable реализация виртуальных функций. См. http://en.wikipedia.org/wiki/Branch_prediction#Prediction_of_indirect_jumps
Если процессор уже имеет адрес памяти в кэше, то выполнение инструкции загрузки тривиально, если это.
Я думаю, что происходит то, что у процессора есть специальный кеш, в котором хранятся местоположения и цели ветвей и непрямых переходов. Если косвенный переход встречается по адресу $ 12345678, и в последний раз он перешел по адресу $ 12348765, процессор может начать спекулятивное выполнение инструкций по адресу $ 12348765 даже до того, как он разрешит адрес ветви. Во многих случаях внутри внутреннего цикла функции конкретный косвенный переход всегда будет переходить на один и тот же адрес на протяжении всего цикла. Таким образом, кэш косвенного перехода может избежать штрафов за ветвление.