Виртуальные функции и производительность - С++

В моем классе я использую абстрактные классы и виртуальные функции. У меня было ощущение, что виртуальные функции влияют на производительность. Это правда? Но я думаю, что это различие в производительности не заметно и похоже, что я делаю преждевременную оптимизацию. Правильно?

Ответ 1

Хорошее эмпирическое правило:

Это не проблема производительности, пока вы не сможете это доказать.

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

Отличная статья, в которой рассказывается о виртуальных функциях (и т.д.), Указатели функций-членов и самые быстрые делегаты С++.

Ответ 2

Ваш вопрос вызвал у меня любопытство, поэтому я пошел вперед и запустил некоторые тайминги на процессоре PowerPC с частотой 3 ГГц, с которым мы работаем. Тест, который я выполнял, состоял в том, чтобы создать простой векторный класс 4d с функциями get/set

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Затем я устанавливаю три массива, каждый из которых содержит 1024 этих векторов (достаточно мал, чтобы соответствовать L1) и запускал цикл, который добавлял их друг к другу (A.x = B.x + C.x) 1000 раз. Я запускал это с функциями, определенными как inline, virtual, и регулярными вызовами функций. Вот результаты:

  • inline: 8 мс (0,65 нс на звонок)
  • direct: 68 мс (5,53 нс на звонок)
  • виртуальный: 160 мс (13 нс на звонок)

Итак, в этом случае (где все подходит в кеше) вызовы виртуальных функций были примерно в 20 раз медленнее, чем встроенные вызовы. Но что это значит? Каждая поездка через цикл вызывала в точности 3 * 4 * 1024 = 12,288 вызовы функций (1024 вектора раз четыре компонента раз три вызова на добавление), поэтому эти времена представляют 1000 * 12,288 = 12,288,000 вызовы функций. Виртуальный цикл занимал 92 мс больше, чем прямой цикл, поэтому дополнительные накладные расходы для каждого вызова составляли 7 наносекунд за каждую функцию.

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

См. также: сравнение сгенерированной сборки.

Ответ 3

Когда Objective-C (где все методы являются виртуальными) является основным языком для iPhone, а freakin 'Java является основным языком для Android, я думаю, что довольно безопасно использовать виртуальные функции С++ на наших трехъядерных башнях 3 ГГц.

Ответ 4

Со страницы 44 из Руководство Agner Fog "Оптимизация программного обеспечения в С++" :

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

Ответ 5

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

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

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

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

Ответ 6

абсолютно. Это было проблемой, когда компьютеры работали со скоростью 100 МГц, так как каждый вызов метода требовал поиска на vtable до его вызова. Но сегодня.. на CPU 3Ghz, который имеет кеш первого уровня с большим объемом памяти, чем мой первый компьютер? Не за что. Выделение памяти из основной ОЗУ будет стоить вам больше времени, чем если бы все ваши функции были виртуальными.

Как старые, старые времена, когда люди говорили, что структурированное программирование было медленным, потому что весь код был разделен на функции, каждая функция требовала распределения стека и вызова функции!

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

PS думают о других "простых в использовании" языках - все их методы виртуальны под обложками, и в настоящее время они не сканируются.

Ответ 7

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

Ответ 8

Есть еще один критерий производительности, кроме времени выполнения. Vtable также занимает пространство памяти, и в некоторых случаях этого можно избежать: ATL использует время компиляции имитированное динамическое связывание templates, чтобы получить эффект" статического полиморфизма ", который трудно объяснить; вы в основном передаете производный класс в качестве параметра шаблону базового класса, поэтому во время компиляции базовый класс" знает", что его производный класс находится в каждом экземпляре. Не позволит вам хранить несколько разных производных классов в наборе базовых типов (этот полиморфизм во время выполнения), но из статического смысла, если вы хотите создать класс Y, который будет таким же, как и ранее существовавший шаблонный класс X, который имеет крючки для такого рода переопределения, вам просто нужно переопределить методы, о которых вы заботитесь, а затем вы получите базовые методы класса X без необходимости иметь vtable.

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

См. также этот другой вопрос SO.

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

Ответ 9

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

Это хорошо иллюстрируется тестом, разница во времени ~ 700% (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

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

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

Ответ 10

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

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

Ответ 11

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

Является ли производительность хитом заметным на современном ноутбуке/настольном компьютере/планшете... наверное, нет! Однако в некоторых случаях со встроенными системами поражение производительности может быть фактором, влияющим на эффективность вашего кода, особенно если виртуальная функция вызывается снова и снова в цикле.

Вот какой-то датированный документ, который анализирует лучшие практики для C/С++ во встраиваемых системах: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

В заключение: это касается программиста, чтобы понять плюсы и минусы использования определенной конструкции над другой. Если вы не управляетесь с высокой производительностью, вам, вероятно, не важно, как это повлияет на производительность, и следует использовать все аккуратные элементы OO на С++, чтобы сделать ваш код максимально удобным.

Ответ 12

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

Ответ 13

Следует отметить, что это:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

может быть быстрее, чем это:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

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

Я говорю "может", потому что это зависит от компилятора, кэша и т.д.

Ответ 14

Снижение производительности при использовании виртуальных функций никогда не сможет снизить преимущества, которые вы получаете на уровне разработки. Предположительно, вызов виртуальной функции будет на 25% менее эффективным, чем прямой вызов статической функции. Это связано с тем, что на VMT существует уровень косвенности. Однако время, затрачиваемое на выполнение вызова, обычно очень мало по сравнению с временем, затраченным на фактическое выполнение вашей функции, поэтому общая стоимость работы будет невыполнимой, особенно при текущей производительности аппаратного обеспечения. Кроме того, компилятор иногда может оптимизировать и видеть, что виртуальный вызов не требуется и скомпилировать его в статический вызов. Поэтому не беспокойтесь, используйте виртуальные функции и абстрактные классы столько, сколько вам нужно.

Ответ 15

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

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

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

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