Избегать виртуальных функций
Итак, предположим, я хочу создать серию классов, каждый из которых имеет функцию-член с одной и той же вещью. Давайте назовем функцию
void doYourJob();
Я хочу в конечном итоге поместить все эти классы в один и тот же контейнер, чтобы я мог их циклически проходить и каждый из них выполнял doYourJob()
Очевидное решение - создать абстрактный класс с функцией
virtual void doYourJob();
но я не решаюсь сделать это. Это дорогостоящая программа, и виртуальная функция значительно снизит ее. Кроме того, эта функция - единственное, что классы имеют общего друг с другом, и doYourJob реализуется совершенно по-разному для каждого класса.
Есть ли способ избежать использования абстрактного класса с виртуальной функцией, или мне придется смириться с этим?
4 ответа
Виртуальные функции не стоят дорого. Это косвенный вызов, в основном как указатель на функцию. Какова производительность при наличии виртуального метода в классе C++?
Если вы находитесь в ситуации, когда каждый цикл на один вызов имеет значение, то есть вы выполняете очень мало работы в вызове функции и вызываете его из своего внутреннего цикла в приложении, критичном к производительности, вам, вероятно, потребуется совсем другой подход.
Если вам нужна скорость, рассмотрите возможность встраивания "типа (-идентифицирующего) числа" в объекты и использования оператора switch для выбора кода, специфичного для типа. Это может полностью избежать накладных расходов на вызовы функций - просто выполнить локальный переход. Вы не получите быстрее, чем это. Стоимость (с точки зрения удобства сопровождения, зависимостей перекомпиляции и т. Д.) Заключается в принудительной локализации (в коммутаторе) функциональности конкретного типа.
РЕАЛИЗАЦИЯ
#include <iostream>
#include <vector>
// virtual dispatch model...
struct Base
{
virtual int f() const { return 1; }
};
struct Derived : Base
{
virtual int f() const { return 2; }
};
// alternative: member variable encodes runtime type...
struct Type
{
Type(int type) : type_(type) { }
int type_;
};
struct A : Type
{
A() : Type(1) { }
int f() const { return 1; }
};
struct B : Type
{
B() : Type(2) { }
int f() const { return 2; }
};
struct Timer
{
Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
struct timespec from;
double elapsed() const
{
struct timespec to;
clock_gettime(CLOCK_MONOTONIC, &to);
return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
}
};
int main(int argc)
{
for (int j = 0; j < 3; ++j)
{
typedef std::vector<Base*> V;
V v;
for (int i = 0; i < 1000; ++i)
v.push_back(i % 2 ? new Base : (Base*)new Derived);
int total = 0;
Timer tv;
for (int i = 0; i < 100000; ++i)
for (V::const_iterator i = v.begin(); i != v.end(); ++i)
total += (*i)->f();
double tve = tv.elapsed();
std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';
// ----------------------------
typedef std::vector<Type*> W;
W w;
for (int i = 0; i < 1000; ++i)
w.push_back(i % 2 ? (Type*)new A : (Type*)new B);
total = 0;
Timer tw;
for (int i = 0; i < 100000; ++i)
for (W::const_iterator i = w.begin(); i != w.end(); ++i)
{
if ((*i)->type_ == 1)
total += ((A*)(*i))->f();
else
total += ((B*)(*i))->f();
}
double twe = tw.elapsed();
std::cout << "switched: " << total << ' ' << twe << '\n';
// ----------------------------
total = 0;
Timer tw2;
for (int i = 0; i < 100000; ++i)
for (W::const_iterator i = w.begin(); i != w.end(); ++i)
total += (*i)->type_;
double tw2e = tw2.elapsed();
std::cout << "overheads: " << total << ' ' << tw2e << '\n';
}
}
РЕЗУЛЬТАТЫ ДЕЯТЕЛЬНОСТИ
В моей системе Linux:
~/dev g++ -O2 -o vdt vdt.cc -lrt
~/dev ./vdt
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726
Это говорит о том, что встроенный подход с переключением числа типов примерно в (1,28–0,23) / (0,344–0,23) = 9,2 раза быстрее. Конечно, это зависит от конкретной тестируемой системы / флагов и версии компилятора и т. Д., Но в целом показательно.
КОММЕНТАРИИ RE ВИРТУАЛЬНАЯ ОТПРАВКА
Однако следует отметить, что издержки вызова виртуальных функций редко бывают значительными, и то только для часто называемых тривиальных функций (таких как методы получения и установки). Даже в этом случае вы можете предоставить единственную функцию для получения и установки множества вещей одновременно, минимизируя затраты. Люди слишком сильно беспокоятся о виртуальной рассылке - так что делайте профилирование, прежде чем искать неловкие альтернативы. Основная проблема с ними заключается в том, что они выполняют вызов функции вне линии, хотя они также делокализуют исполняемый код, который изменяет шаблоны использования кэша (к лучшему или (чаще) к худшему).
Боюсь, что серия dynamic_cast
проверки в цикле снизили бы производительность хуже, чем виртуальная функция. Если вы собираетесь выбросить их все в один контейнер, у них должен быть какой-то общий тип, так что вы можете также сделать его чисто виртуальным базовым классом с этим методом.
В этом контексте виртуальная функция не имеет ничего особенного: поиск в vtable, корректировка поставляемого this
указатель и косвенный вызов.
Если производительность настолько критична, вы можете использовать отдельный контейнер для каждого подтипа и обрабатывать каждый контейнер независимо. Если порядок важен, вы будете делать так много бэкфлипов, что виртуальная отправка, вероятно, будет быстрее.
Если вы собираетесь хранить все эти объекты в одном и том же контейнере, то либо вам придется написать гетерогенный тип контейнера (медленный и дорогой), вам придется хранить контейнер с void *
s (чёрт!), или классы должны быть связаны друг с другом через наследование. Если вы решите использовать любой из первых двух вариантов, вам нужно будет иметь некоторую логику, чтобы посмотреть на каждый элемент в контейнере, выяснить, к какому типу он относится, а затем вызвать соответствующий doYourJob()
реализация, которая по сути сводится к наследованию.
Я настоятельно рекомендую сначала попробовать простой и понятный подход к использованию наследования. Если это достаточно быстро, это здорово! Вы сделали Если это не так, попробуйте использовать другую схему. Никогда не избегайте полезной языковой функции из-за стоимости, если у вас нет веских доказательств того, что цена слишком велика.