0

Виртуальные вызовы функций могут быть медленными из-за виртуальных вызовов, требующих дополнительного индексированного отношения к таблице v, что может привести к промаху кэша данных, а также кэш кеша команд ... Не подходит для критически важных приложений.C++ virtual function vs function function pointer (сравнение производительности)

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

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

Мне просто интересно, является ли этот метод жизнеспособной заменой для виртуального вызова парадигма, если да, то почему она не более вездесущая?

Заранее благодарим за ваше время! :)

class BaseClass 
{ 
protected: 

    // member function pointer 
    typedef void(BaseClass::*FooMemFuncPtr)(); 
    FooMemFuncPtr m_memfn_ptr_Foo; 

    void FooBaseClass() 
    { 
     printf("FooBaseClass() \n"); 
    } 

public: 

    BaseClass() 
    { 
     m_memfn_ptr_Foo = &BaseClass::FooBaseClass; 
    } 

    void Foo() 
    { 
     ((*this).*m_memfn_ptr_Foo)(); 
    } 
}; 

class DerivedClass : public BaseClass 
{ 
protected: 

    void FooDeriveddClass() 
    { 
     printf("FooDeriveddClass() \n"); 
    } 

public: 

    DerivedClass() : BaseClass() 
    { 
     m_memfn_ptr_Foo = (FooMemFuncPtr)&DerivedClass::FooDeriveddClass; 
    } 
}; 

int main(int argc, _TCHAR* argv[]) 
{ 
    DerivedClass derived_inst; 
    derived_inst.Foo(); // "FooDeriveddClass()" 

    BaseClass base_inst; 
    base_inst.Foo(); // "FooBaseClass()" 

    BaseClass * derived_heap_inst = new DerivedClass; 
    derived_heap_inst->Foo(); 

    return 0; 
} 
+3

1. Профилируйте код, прежде чем задавать такие вопросы. То, что вы в основном говорите, это «профайл кода для меня». 2. Посмотрите на полиморфизм времени компиляции. –

+0

Старый, но может быть интересен для вас: http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible – PlasmaHH

+0

Да, я планирую профайл кода, но мне было любопытно если есть какие-либо концептуальные различия в производительности – eddietree

ответ

1

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

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

Использование функции «указатель-член», вероятно, даже хуже, чем PTF, скорее всего, она будет использовать ту же структуру VMT для аналогичного смещенного доступа, вместо переменной вместо переменной.

+0

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

+0

true, но кеши большие, а код маленький. Переключатель контекста действительно представляет большую угрозу, чем v-таблица. – Mgetz

+0

Невозможно рассуждать о промахах кеша, но измерять. Но представленная альтернатива, похоже, имеет по крайней мере одинаковое количество направлений ... –

0

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

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

+0

Большое спасибо за ваш ответ, но можете ли вы объяснить, что вы имеете в виду «doesn», t work ", и как предсказание отрасли вступит в игру для моего конкретного примера? – eddietree

+0

В принципе, это предварительная оптимизация, которая вытесняется, но тот факт, что вы используете код в современной несовместимой многозадачной операционной системе. Кроме того, TLB действительно хорош в прогнозировании того, что будет использоваться позже, и поскольку функции в vTable, как правило, находятся в одной кодовой странице, они почти всегда будут в кеше. – Mgetz

2

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

Это не совсем правильно. Vtable должен вычисляться при построении объекта, причем каждый указатель виртуальной функции устанавливается в наиболее специализированную версию в иерархии. Процесс вызова виртуальной функции не выполняет итерацию указателей, но вызывает что-то вроде *(vtbl_address + 8)(args);, которое вычисляется в постоянное время.

, который может привести к промаху кэша данных, а также кэш кеша команд ... Не подходит для приложений с критическими характеристиками.

Ваше решение не подходит для приложений с критическими характеристиками (в общем), поскольку оно является общим.

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

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

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

Edit:

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

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

3

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

$ time ./main 1 
Using member pointer 

real 0m3.343s 
user 0m3.340s 
sys  0m0.002s 

$ time ./main 2 
Using virtual function call 

real 0m2.227s 
user 0m2.219s 
sys  0m0.006s 

Вот код:

#include <cstdlib> 
#include <cstring> 
#include <iostream> 
#include <stdio.h> 

struct BaseClass 
{ 
    typedef void(BaseClass::*FooMemFuncPtr)(); 
    FooMemFuncPtr m_memfn_ptr_Foo; 

    void FooBaseClass() { } 

    BaseClass() 
    { 
     m_memfn_ptr_Foo = &BaseClass::FooBaseClass; 
    } 

    void Foo() 
    { 
     ((*this).*m_memfn_ptr_Foo)(); 
    } 
}; 

struct DerivedClass : public BaseClass 
{ 
    void FooDerivedClass() { } 

    DerivedClass() : BaseClass() 
    { 
     m_memfn_ptr_Foo = (FooMemFuncPtr)&DerivedClass::FooDerivedClass; 
    } 
}; 

struct VBaseClass { 
    virtual void Foo() = 0; 
}; 

struct VDerivedClass : VBaseClass { 
    virtual void Foo() { } 
}; 

static const size_t count = 1000000000; 

static void f1(BaseClass* bp) 
{ 
    for (size_t i=0; i!=count; ++i) { 
    bp->Foo(); 
    } 
} 

static void f2(VBaseClass* bp) 
{ 
    for (size_t i=0; i!=count; ++i) { 
    bp->Foo(); 
    } 
} 

int main(int argc, char** argv) 
{ 
    int test = atoi(argv[1]); 
    switch (test) { 
     case 1: 
     { 
      std::cerr << "Using member pointer\n"; 
      DerivedClass d; 
      f1(&d); 
      break; 
     } 
     case 2: 
     { 
      std::cerr << "Using virtual function call\n"; 
      VDerivedClass d; 
      f2(&d); 
      break; 
     } 
    } 

    return 0; 
} 

Составлено с использованием:

g++ -O2 main.cpp -o main 

с г ++ 4.7.2.

+0

очень интересно .. Большое спасибо за его профилирование! Интересно, насколько это связано с тем, что vtable и инструкции являются свежими в кеше. – eddietree

+0

Это также может быть связано с тем, что виртуальные таблицы были на C++ в течение длительного времени, и, следовательно, авторы компиляторов узнали, как и когда их оптимизировать. Где, как и в вашем коде, компилятор может сделать меньше допущений и должен делать менее оптимальную вещь. – Daemin

0

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

Кроме того, имея указатель на таблицу виртуальных функций, пространственная сложность виртуальной функции - это O (1) (только указатель). С другой стороны, если вы сохраняете указатели функций внутри класса, то сложность O (N) (ваш класс теперь содержит столько указателей, сколько есть «виртуальных» функций). Если есть много функций, вы платите за это - при предварительной загрузке объекта вы загружаете все указатели в строке кэша, а не только один указатель и первые несколько членов, которые вам, скорее всего, понадобятся. Это похоже на отходы.

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

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

Как правило, не беспокойтесь о языковых конструкциях, потому что они кажутся незнакомыми. Беспокоитесь и оптимизируйте только после того, как вы измерили и определили, где узкое место действительно.