Приложения AI в C++: Насколько дорогостоящий виртуальные функции? Какова возможная оптимизация?

16
задан Community 23 May 2017 в 12:34
поделиться

15 ответов

Виртуальные функции очень эффективны. При принятии указателей на 32 бита расположение памяти приблизительно:

classptr -> [vtable:4][classdata:x]
vtable -> [first:4][second:4][third:4][fourth:4][...]
first -> [code:x]
second -> [code:x]
...

classptr указывает на память, которая обычно находится на "куче", иногда на стеке, и запускается с четырехбайтового указателя на vtable для того класса. Но важной вещью помнить является само vtable, не выделенная память. Это - статический ресурс, и все объекты того же типа класса укажут на точно та же ячейка памяти для их vtable массива. Обращение к различным экземплярам не вытянет различные ячейки памяти в кэш L2.

Этот пример от msdn показывает vtable для класса A с виртуальным func1, func2, и func3. Ничто больше чем 12 байтов. Существует хороший шанс, vtables различных классов также будет физически смежен в скомпилированной библиотеке (Вы захотите проверить, что это, Вы особенно заинтересованы), который мог увеличить эффективность кэша тщательно.

CONST SEGMENT
??_7A@@6B@
   DD  FLAT:?func1@A@@UAEXXZ
   DD  FLAT:?func2@A@@UAEXXZ
   DD  FLAT:?func3@A@@UAEXXZ
CONST ENDS

другая проблема производительности была бы инструкцией наверху вызова через vtable функцию. Это также очень эффективно. Почти идентичный вызову невиртуальной функции. Снова от пример от msdn:

; A* pa;
; pa->func3();
mov eax, DWORD PTR _pa$[ebp]
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR _pa$[ebp]
call  DWORD PTR [edx+8]

В этом примере ebp, указателе базы стекового фрейма, имеет переменную A* pa при нулевом смещении. Регистр eax загружается значением в местоположении [ebp], таким образом, это имеет*, и edx загружается значением в местоположении [eax], таким образом, это имеет vtable класс A. Тогда ecx загружается [ebp], потому что ecx представляет "это", это теперь содержит*, и наконец вызов выполняется к значению в местоположении [edx+8], который является третьим функциональным адресом в vtable.

, Если бы этот вызов функции не был виртуальным, mov eax и mov edx не были бы необходимы, но разница в производительности была бы неизмеримо небольшой.

28
ответ дан 30 November 2019 в 15:16
поделиться

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

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

  1. , Люди записали компиляторы, которые обращаются к анализу кода и преобразованию программы. Но, это не производственный класс компиляторы.
  2. Вы могли заменить все виртуальные функции эквивалентным "переключателем... случай" блоки для вызывания соответствующих функций на основе типа в иерархии. Таким образом, Вы избавитесь от управляемой виртуальной таблицы компилятора, и у Вас будет своя собственная виртуальная таблица в форме переключателя... блоком случая. Теперь, возможности Вашей собственной виртуальной таблицы, находящейся в кэше L2, высоки как он в пути выполнения кода. Помните, Вам будут нужны RTTI или Ваша собственная функция "typeof" для достижения этого.
0
ответ дан 30 November 2019 в 15:16
поделиться

С современными, выглядящими вперед, несколькими диспетчеризирующими центральными процессорами издержки для виртуальной функции могли бы хорошо быть нулем. Nada. Zip.

1
ответ дан 30 November 2019 в 15:16
поделиться

Стоимость является более или менее тем же, чем нормальные функции в наше время для недавних CPU, но они не могут быть встроены. При вызове функции миллионы времен влияние может быть значительным (попытайтесь назвать миллионы времен той же функцией, например, однажды со встроенным однажды без, и Вы будете видеть, что это может быть вдвое медленнее, если сама функция делает что-то простое; это не теоретический случай: это довольно характерно для большого числового вычисления).

1
ответ дан 30 November 2019 в 15:16
поделиться

Редко необходимо волноваться о кэше в отношении таких наиболее часто используемых объектов, так как они выбраны однажды и сохранены там.

Кэш является только обычно проблемой при контакте с большими структурами данных что также:

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

Дела как Vtables обычно не идут быть производительностью/кэшем/проблемой памяти; обычно существует только один Vtable на тип объекта, и объект содержит указатель на Vtable вместо самого Vtable. Таким образом, если у Вас нет нескольких тысяч типов объектов, я не думаю, что Vtables собираются перегрузить Ваш кэш.

1), между прочим, то, почему функции как memcpy используют инструкции по потоковой передаче обхода кэша как movnt (dq|q) для чрезвычайно большого (мультимегабайт) вводы данных.

1
ответ дан 30 November 2019 в 15:16
поделиться

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

Что-то можно сделать для предотвращения потерь того средства в некоторых ситуациях, должен объявить функцию, встроенную виртуальный.

Class A {
   inline virtual int foo() {...}
};

И когда Вы в точке кода, Вы УВЕРЕНЫ в типе называемого объекта, можно выполнить встроенный вызов, который избежит полиморфной системы и позволит встроить компилятором.

class B : public A {
     inline virtual int foo() 
     {
         //...do something different
     }

     void bar()
     {
      //logic...
      B::foo();
      // more  logic
     }
};

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

2
ответ дан 30 November 2019 в 15:16
поделиться

Я укрепляю все ответы, в которых говорится в действительности:

  • , Если Вы на самом деле не знаете, что это - проблема, любая озабоченность по поводу фиксации его, вероятно, неуместна.

то, Что Вы хотите знать:

  • , Что часть времени выполнения (когда это на самом деле работает) потрачена в процессе вызова методов, и, в частности, какие методы являются самыми дорогостоящими (этой мерой).

Некоторые профилировщики могут дать Вам эту информацию косвенно. Они должны подвести итог на уровне оператора, но эксклюзивный из времени, проведенного в самом методе.

Моя любимая техника состоит в том, чтобы просто приостановить его неоднократно под отладчиком.

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

, Если Вы на самом деле не видите, что, это не проблема.

В процессе, Вы будете, вероятно, видеть другие вещи выше стек вызовов, которые на самом деле не нужны и могли сохранить Вас много времени.

2
ответ дан 30 November 2019 в 15:16
поделиться

Решением динамического полиморфизма мог быть статический полиморфизм, применимый, если Ваши типы известны в типе компиляции: CRTP (Любопытно повторяющийся шаблонный шаблон).

http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

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

2
ответ дан 30 November 2019 в 15:16
поделиться

Можно реализовать полиморфизм во времени выполнения с помощью виртуальных функций и во время компиляции при помощи шаблонов. Можно заменить виртуальные функции шаблонами. Смотрите на эту статью для получения дополнительной информации - http://www.codeproject.com/KB/cpp/SimulationofVirtualFunc.aspx

2
ответ дан 30 November 2019 в 15:16
поделиться

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

Оптимизация обычно вращается вокруг выражения изменчивости выше в стеке вызовов так, чтобы Вы не должны были вызывать виртуальную функцию многократно в горячих точках.

3
ответ дан 30 November 2019 в 15:16
поделиться

Единственная оптимизация, о которой я могу думать, является JIT-компилятором Java. Если я понимаю его правильно, это контролирует вызовы, когда код работает, и если большинство вызовов переходит к конкретной реализации только, это вставляет условный переход в реализацию, когда класс является правильным. Таким образом, большую часть времени, нет никакого vtable поиска. Конечно, для редкого случая, когда мы передаем различный класс, vtable, все еще используется.

я не знаю ни о каком компиляторе/времени выполнения C++, который использует эту технику.

3
ответ дан 30 November 2019 в 15:16
поделиться

Вы на самом деле представили и нашли, где, и чему нужна оптимизация?

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

3
ответ дан 30 November 2019 в 15:16
поделиться

Разделите 5.3.3 из эти , готовят Технический Отчет на Производительности C++ , полностью посвящен издержкам виртуальных функций.

11
ответ дан 30 November 2019 в 15:16
поделиться

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

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

И конечно это также зависит от архитектуры ЦП. На некоторых это может стать довольно дорогим.

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

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

, Но сначала, удостоверьтесь, что у Вас есть проблема. Код является на самом деле слишком медленным, чтобы быть приемлемым? Во-вторых, узнайте то, что заставляет его замедлиться через профилировщика. И в-третьих, зафиксируйте его.

2
ответ дан 30 November 2019 в 15:16
поделиться

Статический полиморфизм, как здесь ответили некоторые пользователи. Например, WTL использует этот метод. Четкое объяснение реализации WTL можно найти на http://www.codeproject.com/KB/wtl/wtl4mfc1.aspx#atltemplates

2
ответ дан 30 November 2019 в 15:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: