Виртуальная функция C++ против указателя на функцию-член (сравнение производительности)
Вызовы виртуальных функций могут быть медленными из-за виртуальных вызовов, требующих дополнительной индексированной привязки к v-таблице, что может привести к потере кеша данных, а также кешу кеша инструкций... Не подходит для приложений, критичных для производительности.
Поэтому я подумал о том, как преодолеть эту проблему производительности виртуальных функций, но при этом иметь некоторые из тех же функций, которые предоставляют виртуальные функции.
Я уверен, что это было сделано раньше, но я разработал простой тест, который позволяет базовому классу хранить указатель на функцию-член, который может быть установлен любым производным классом. И когда я вызываю 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;
}
5 ответов
Я провел тест, и версия с использованием вызовов виртуальных функций была быстрее в моей системе с оптимизацией.
$ 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
с g++ 4.7.2.
Вызовы виртуальных функций могут быть медленными из-за того, что виртуальным вызовам приходится обходить виртуальную таблицу,
Это не совсем правильно. Таблица vtable должна быть вычислена при построении объекта, при этом для каждого указателя виртуальной функции должна быть установлена наиболее специализированная версия в иерархии. Процесс вызова виртуальной функции не повторяет указатели, а вызывает что-то вроде *(vtbl_address + 8)(args);
, который вычисляется в постоянное время.
Это может привести к потере кеша данных, а также кешу инструкций... Не подходит для приложений, критичных к производительности.
Ваше решение не подходит для приложений, критичных к производительности (в целом), потому что оно универсально.
Как правило, приложения, критичные к производительности, оптимизируются для каждого конкретного случая (измеряйте, выбирайте код с наихудшими проблемами производительности в модуле и оптимизируйте).
При таком подходе к конкретному случаю у вас, вероятно, никогда не будет случая, когда ваш код будет медленным, потому что компилятор должен пройти vtbl. Если это так, то медлительность, вероятно, будет вызвана вызовом функций через указатели, а не напрямую (т. Е. Проблема будет решена путем встраивания, а не путем добавления дополнительного указателя в базовый класс).
В любом случае, все это академично, пока у вас нет конкретного случая для оптимизации (и вы измерили, что ваш худший нарушитель - это вызовы виртуальных функций).
Редактировать:
Мне просто интересно, если этот метод является жизнеспособной заменой парадигме виртуального вызова, если так, почему он не является более вездесущим?
Поскольку это выглядит как универсальное решение (повсеместное применение которого приведет к снижению производительности вместо улучшения), решение несуществующей проблемы (ваше приложение обычно не замедляется из-за вызовов виртуальных функций).
Виртуальные функции не "пересекают" таблицу, просто выполняют выборку указателя из местоположения и вызывают этот адрес. Это как если бы вы имели ручную реализацию указателя на функцию и использовали ее для вызова вместо прямого.
Таким образом, ваша работа хороша только для обфускации и саботирует случаи, когда компилятор может выполнить прямой вызов без виртуальных функций.
Использование функции указателя на член, вероятно, даже хуже, чем PTF, скорее всего, она будет использовать ту же структуру VMT для аналогичного удаленного доступа, только переменную вместо фиксированной.
На самом деле некоторые компиляторы могут использовать thunks, которые сами переводят на обычные указатели функций, поэтому в основном компилятор делает для вас то, что вы пытаетесь сделать вручную (и, вероятно, запутывает людей).
Кроме того, имея указатель на таблицу виртуальных функций, сложность пространства виртуальной функции составляет O(1) (только указатель). С другой стороны, если вы храните указатели на функции внутри класса, то сложность составляет O(N) (теперь ваш класс содержит столько указателей, сколько имеется "виртуальных" функций). Если функций много, вы платите за это - при предварительной выборке вашего объекта вы загружаете все указатели в строке кэша, а не только один указатель и первые несколько членов, которые вам, вероятно, понадобятся. Это звучит как пустая трата времени.
Таблица виртуальных функций, с другой стороны, находится в одном месте для всех объектов одного типа и, вероятно, никогда не будет выталкиваться из кэша, пока ваш код вызывает некоторые короткие виртуальные функции в цикле (что, вероятно, является проблемой, когда виртуальная функция стоимость станет узким местом).
Что касается предсказания ветвлений, в некоторых случаях простое дерево решений по типу объекта и встроенные функции для каждого конкретного типа дают хорошую производительность (тогда вы сохраняете информацию о типе вместо указателя). Это не применимо ко всем типам проблем и будет в основном преждевременной оптимизацией.
Как правило, не беспокойтесь о языковых конструкциях, потому что они кажутся незнакомыми. Беспокойство и оптимизация только после того, как вы измерили и определили, где на самом деле узкое место.
Главным образом потому, что это не работает. Большинство современных процессоров лучше предсказывают переходы и спекулятивное выполнение, чем вы думаете. Однако мне еще предстоит увидеть процессор, который выполняет спекулятивное выполнение за пределами нестатической ветви.
Кроме того, в современном процессоре у вас больше шансов пропустить кеш, потому что у вас был переключение контекста непосредственно перед вызовом, а другая программа заняла кеш, чем вы, из-за v-таблицы, даже этот сценарий очень отдаленная возможность,