Реализация виртуальной функции - PullRequest
2 голосов
/ 09 марта 2010

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

Допустим, у меня есть базовый класс с 2 подклассами (X и Y) и одной виртуальной функцией, поэтому будет две виртуальные таблицы. У объекта есть указатель, на основании которого он будет выбирать виртуальную таблицу. Так что для компилятора это больше похоже на

switch( object's function ptr )
{

   case 0x....:

       X->call();

       break;

   case 0x....:

       Y->call();
};

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

Спасибо, Gokul.

Ответы [ 8 ]

2 голосов
/ 09 марта 2010

Компилятор не может этого сделать из-за отдельной модели компиляции.

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

Рассмотрим этот код:

// base.h
class base
{
public:
    virtual void doit();
};

и это:

// usebase.cpp
#include "base.h"

void foo(base &b)
{
    b.doit();
}

Когда компилятор генерирует виртуальный вызов в foo, он не знает, какие подклассы base будут существовать во время выполнения.

1 голос
/ 09 марта 2010

Вот некоторые результаты конкретных испытаний. Эти конкретные результаты взяты из VC ++ 9.0 / x64:

Test Description: Time to test a global using a 10-way if/else if statement
CPU Time:        7.70  nanoseconds           plus or minus      0.385

Test Description: Time to test a global using a 10-way switch statement
CPU Time:        2.00  nanoseconds           plus or minus     0.0999

Test Description: Time to test a global using a 10-way sparse switch statement
CPU Time:        3.41  nanoseconds           plus or minus      0.171

Test Description: Time to test a global using a 10-way virtual function class
CPU Time:        2.20  nanoseconds           plus or minus      0.110

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

1 голос
/ 09 марта 2010

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

  • Операторы переключения не обязательно быстрее, чем вызовы виртуальных функций, или встроенными.Вы можете узнать больше о том, как операторы switch превращаются в сборку здесь и здесь .
  • В вызовах виртуальных функций медленно работает не поиск указателей, а косвенная ветвь.По сложным причинам , связанным с внутренней электроникой ЦП , для большинства современных процессоров быстрее выполнить "прямую ветвь", где адрес назначения закодирован в инструкции, чем " косвенная ветвь", где адрес вычисляется во время выполнения.Вызовы виртуальных функций и операторы большого коммутатора обычно реализуются как косвенные ветви.
  • В приведенном выше примере коммутатор полностью избыточен.Как только указатель на функцию-член объекта был вычислен, ЦПУ может перейти прямо к нему.Даже если компоновщик знает обо всех возможных объектах-членах, которые существуют в исполняемом файле, все равно нет необходимости добавлять этот поиск в таблицу.
0 голосов
/ 09 марта 2010

Подобные оптимизации возможны только с помощью компоновщика повторной компоновки, который должен работать как часть среды выполнения C ++.

Среда выполнения C ++ более сложна, так что даже новая загрузка DLL (с ​​COM) добавит новые указатели функций в vtable. (думаете о чистых виртуальных фнс?)

Тогда компилятор или компоновщик не могут выполнить эту оптимизацию. Параметр switch / case явно быстрее, чем косвенный вызов, поскольку предварительная выборка в ЦП является детерминированной и возможна конвейерная обработка. Но это не сработает в C ++ из-за этого расширения среды выполнения vtable объекта.

0 голосов
/ 09 марта 2010

Окончательное утверждение о том, что switch/case более или менее эффективен, чем виртуальные вызовы, является чрезмерным обобщением. Правда в том, что это будет зависеть от многих вещей и будет зависеть от:

  • какой компилятор вы используете
  • какие оптимизации включены
  • общие характеристики вашей программы и то, как они влияют на эти оптимизации

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

0 голосов
/ 09 марта 2010

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

0 голосов
/ 09 марта 2010

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

В ореховой оболочке одна общая упрощенная реализация виртуальной функции C ++: каждый класс будет иметь виртуальную таблицу (vbtl), а каждый экземпляр класса будет иметь указатель виртуальной таблицы (vptr). Виртуальная таблица - это в основном список указателей функций.

Когда вы вызываете виртуальную функцию, говорите, что это похоже на:

class Base {};
class Derived {};
Base* pB = new Derived();
pB->someVirtualFunction();

Функция someVirtualFunction () будет иметь соответствующий индекс в vtbl. И звонок

pB->someVirtualFunction(); 

будет преобразован во что-то вроде:

pB->vptr[k](); //k is the index of the 'someVirtualFunction'.

Таким образом, функция фактически вызывается косвенно и имеет полиморфизм.

Предлагаю вам прочитать 'Объектная модель C ++' Стенли Липпмана.

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

0 голосов
/ 09 марта 2010

В виртуальной рассылке нет разветвлений. Vptr в вашем классе указывает на vtable со вторым указателем для конкретной функции с постоянным смещением.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...