Стоимость виртуальной функции в тесной петле - PullRequest
5 голосов
/ 06 июля 2011

Я нахожусь в ситуации, когда у меня есть игровые объекты с виртуальной функцией Update ().Есть много игровых объектов (в настоящее время их чуть больше 7000), и цикл вызывает обновление для всех из них (среди прочего).Мой коллега предложил полностью удалить виртуальную функцию.Как вы можете себе представить, это займет много рефакторинга.

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

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

РЕДАКТИРОВАТЬ : Есть несколько ответов, которые дают отличное понимание, поэтому любой, кто наткнется на этот вопрос в будущем,взгляните на все ответы, а не только на выбранный.

Ответы [ 4 ]

10 голосов
/ 06 июля 2011

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

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

Для справки, 140000 циклов составляет около 50 микросекунд. Это предполагает P4 с огромным конвейером и всегда полным сбросом конвейера (который вы обычно не получаете).

8 голосов
/ 06 июля 2011

Хотя это не тот код и, возможно, он не тот же компилятор, который вы используете, вот несколько справочных данных из довольно старого теста (bench ++ Джо Ороста):

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006


Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005


Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006


Test Name:   F000008                         Class Name:  Style
CPU Time:        2.20  nanoseconds           plus or minus      0.110
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way virtual function class
 compare this test with F000006

Этот конкретный результат получен при компиляции с 64-битной версией VC ++ 9.0 (VS 2008), но он в достаточной степени похож на то, что я видел в других недавних компиляторах. Суть в том, что виртуальная функция быстрее, чем большинство очевидных альтернатив, и очень близка к той же скорости, что и единственная, которая ее побеждает (фактически, оба равных находятся в пределах измеряемой границы ошибки). Это, однако, зависит от плотных значений - как вы можете видеть в F00007, если значения редкие, оператор switch создает код, который медленнее, чем вызов виртуальной функции.

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

6 голосов
/ 06 июля 2011

Если вы не можете профилировать, взгляните на ассемблерный код, чтобы понять, насколько дорогой поиск в действительности.Это может быть простой косвенный переход, который почти ничего не стоит.

Если вам нужно провести рефакторинг, вот предложение: создайте множество классов "UpdateXxx", которые знают, как вызывать новый не виртуальный метод update(),Соберите их в массив и затем вызовите update() для них.

Но я предполагаю, что вы не сэкономите много, особенно не только с 7K объектами.

Примечание по профилированию: ЕслиВы не можете использовать профилировщик (заставляет задуматься, почему нет), время звонков на update() и журнал звонков, которые занимают больше, скажем, 100 мс.Время не дорогое и позволяет быстро определить, какие звонки самые дорогие.

2 голосов
/ 06 июля 2011

другой тест с виртуальными, встроенными и прямыми вызовами вы можете найти здесь [введите описание ссылки здесь] [1] Виртуальные функции и производительность - C ++

...