Избегать виртуальных функций

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

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() реализация, которая по сути сводится к наследованию.

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

Другие вопросы по тегам