Избегать виртуальных функций - PullRequest
9 голосов
/ 26 января 2011

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

void doYourJob();

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

Очевидное решениеэто сделать абстрактный класс с функцией

 virtual void doYourJob();

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

Есть ли способ избежать использования абстрактного класса с виртуальной функцией или мне придется с ним смириться?

Ответы [ 4 ]

8 голосов
/ 26 января 2011

Если вам нужна скорость, рассмотрите возможность встраивания «типа (-идентифицирующего) числа» в объекты и использования оператора 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 ВИРТУАЛЬНАЯ ОТПРАВКА

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

6 голосов
/ 26 января 2011

Виртуальные функции не стоят дорого. Это косвенный вызов, в основном как указатель на функцию. Какова стоимость производительности виртуального метода в классе C ++?

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

4 голосов
/ 26 января 2011

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

Там не всето же самое для виртуальной отправки функций в этом контексте: поиск в vtable, корректировка поставляемого указателя this и косвенный вызов.

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

1 голос
/ 26 января 2011

Если вы собираетесь хранить все эти объекты в одном и том же контейнере, то либо вам придется написать гетерогенный тип контейнера (медленный и дорогой), вам придется хранить контейнер сvoid * s (чёрт!), Или классы должны быть связаны друг с другом через наследование.Если вы решите использовать одну из первых двух опций, вам потребуется некоторая логика, чтобы посмотреть на каждый элемент в контейнере, выяснить, к какому типу он относится, а затем вызвать соответствующую реализацию doYourJob(),по сути, сводится к наследованию.

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

...