Виртуальная функция по сравнению с приведением указателя - PullRequest
3 голосов
/ 16 июля 2011

Текущая версия некоторого кода, который я использую, использует немного странный способ достижения чего-то, что, я думаю, может быть достигнуто полиморфизмом.Более конкретно, в настоящее время мы используем что-то вроде

for(int i=0; i<CObjList.size(); ++i)
{
   CObj* W = CObjList[i];
   if( W->type == someTypeA )
   {
       // do some things which also involve casts such as
       //  ((SomeClassA*) W->objectptr)->someFieldA
   }
   else if( W->type == someTypeB )
   {
       // do some things which also involve casting such as
       //  ((SomeClassB*) W->objectptr)->someFieldB
   }
}

Для пояснения;каждый объект W содержит void *objectptr;, то есть указатель на произвольное местоположение.Поле W->type отслеживает тип объекта, на который указывает objectptr, так что внутри наших операторов if / else мы можем привести W->objectptr к правильному типу и использовать его поля.

Однако это кажетсяизначально плохой с точки зрения разработки кода по нескольким причинам:

  1. Мы не можем гарантировать, что объект, на который указывает W->objectptr, действительно соответствует тому, что сказано в W->type, поэтому приведение является небезопасным.
  2. Каждый раз, когда мы хотим добавить другой тип, мы должны добавить еще один оператор elseif и убедиться, что W->type установлен правильно.

Кажется, что это было бы гораздо лучше решить с помощью чего-токак

class CObj
{
public:
   virtual void doSomething(/* some params */)=0;
};

class SomeClassA : public CObj
{
public:
   virtual void doSomething(/* some params */);
   int someFieldA;
}

class SomeClassB : public CObj
{
public:
   virtual void doSomething(/* some params */);
   int someFieldB;
}

// sometime later...

for(int i=0; i<CObjList.size(); ++i)
{
   CObj* W = CObjList[i];
   W->doSomething(/* some params */);
}

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

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

Ответы [ 6 ]

3 голосов
/ 16 июля 2011

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

Если вы используете процессор x86, PPC или ARM, вам нужны виртуальные функции в этой ситуации.Затраты на производительность при вызове виртуальной функции в основном составляют конвейерный пузырь , вызванный неправильным предсказанием косвенной ветви .Поскольку этап извлечения инструкций ЦП не может знать, куда идет вычисленный jmp, он не может начать выборку байтов с целевого адреса до тех пор, пока не выполнится ветвь, и, таким образом, в конвейере есть остановка, соответствующая числу этапов междуПервый этап извлечения и отделение удаляются.(На PPC я знаю лучше, это что-то вроде 25 циклов.)

У вас также есть задержка загрузки указателя vtable, но это часто скрывается переупорядочением команд (компилятор перемещает инструкцию load так,он запускается за несколько циклов, прежде чем вам действительно понадобится результат, и ЦП выполняет другую работу, пока кеш данных отправляет вам свои электроны.прямых условных переходов - когда цель известна во время компиляции, но выполняется ли переход во время выполнения.(т. е. код операции jump-on-equal .) В этом случае ЦП будет делать предположение (прогнозировать), взята ли каждая ветвь или нет, и начнет получать инструкции соответствующим образом.Таким образом, у вас будет только пузырь, если процессор угадает неправильно.Поскольку вы, вероятно, каждый раз вызываете эту функцию с разными входными данными, она будет неправильно предсказывать хотя бы одну из этих ветвей, и вы получите точно такой же пузырь, как и с виртуальными.На самом деле, у вас будет гораздо больше пузырьков - по одному на условие if ()!

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

3 голосов
/ 16 июля 2011

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

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

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

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

У меня был некоторый опыт работы с каким-то длинным (думаю, 1М + строкой) научным вычислительным кодом, в котором использовалась подобная конструкция переключателя на основе типа.Они переориентировались на подход, основанный на полиморфности, и получили значительное ускорение .Точно противоположное тому, что они ожидали!

Оказалось, что компилятор мог лучше оптимизировать некоторые вещи в этой структуре.

Однако это было давно (8 лет или около того) ..так что кто знает, что будут делать современные компиляторы.Не угадай - профилируй.

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

Мой вопрос тогда; перевешивается ли сложность нескольких операций поиска в vtable из-за улучшенного дизайна и расширяемости кода, и может ли это сильно повлиять на производительность?

Компиляторы C ++ должны уметь очень эффективно реализовывать виртуальные функции, поэтому я не думаю, что есть их недостаток в их использовании. (И, конечно, огромное преимущество в удобстве обслуживания и удобочитаемости!) Но вы должны принять меры, чтобы убедиться.

Способ, которым они обычно реализуются, заключается в том, что каждый объект имеет указатель vtable. (несколько указателей в случае множественного наследования, но давайте пока забудем об этом) Это имеет следующие относительные затраты по сравнению с не виртуальными функциями.

  • пространство данных: один указатель на объект
  • пространство данных: одна таблица на класс (не на объект!)
  • время: наихудший случай = две операции чтения памяти за вызов функции (1 для получения адреса виртуальной таблицы, 1 для получения адреса функции внутри виртуальной таблицы). Смещение в vtable известно во время компиляции, потому что вы знаете, какую функцию вы вызываете. Там нет дополнительных прыжков.

Сравните это с затратами на использование подхода, не связанного с ООП, в существующем программном обеспечении.

  • пространство данных: один идентификатор типа на объект
  • кодовое пространство: по одному дереву if / else или оператору switch каждый раз, когда вы хотите вызвать функцию, зависящую от типа объекта
  • time: необходимо оценить дерево if / else или оператор switch.

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

1 голос
/ 16 июля 2011

Как говорит Петр, правильный ответ - это, вероятно, виртуальные функции. Вам придется проверить.

Но чтобы решить вашу проблему с кастами:

  1. Никогда не используйте приведение в стиле C в программе на C ++, используйте static_cast <>, dynamic_cast <> и т. Д.
  2. В вашем конкретном случае используйте dynamic_cast <>. По крайней мере, тогда вы получите исключение, если типы не связаны должным образом, что лучше, чем дикий сбой.
0 голосов
/ 16 июля 2011

CRTP было бы отличной идеей для таких случаев.

Редактировать : В вашем случае

template<class T>
class CObj
{
public:
   void doSomething(/* some params */)
   {
     static_cast<T*>(this)->doSomething(...);
   }
};

class SomeClassA : public CObj<SomeClassA>
{
public:
   void doSomething(/* some params */);
   int someFieldA;
};

class SomeClassB : public CObj<<SomeClassB>
{
public:
   void doSomething(/* some params */);
   int someFieldB;
};

Теперь выможет потребоваться выбрать код цикла другим способом, чтобы вместить все объекты различного типа CObj<T>.

...