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

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

109 голосов | спросил Navaneeth K N 16 Jam1000000amFri, 16 Jan 2009 11:22:36 +030009 2009, 11:22:36

15 ответов


0

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

  

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

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

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

ответил Greg Hewgill 16 Jam1000000amFri, 16 Jan 2009 11:25:56 +030009 2009, 11:25:56
0

Ваш вопрос вызвал у меня любопытство, поэтому я остановился и запустил некоторые тайминги для процессора PowerPC 3GHz, с которым мы работаем. Тест, который я провел, состоял в том, чтобы создать простой 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 и регулярные вызовы функций. Вот результаты:

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

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

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

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

ответил Crashworks 17 Jam1000000amSat, 17 Jan 2009 11:43:01 +030009 2009, 11:43:01
0

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

ответил Chuck 16 Jam1000000amFri, 16 Jan 2009 11:56:41 +030009 2009, 11:56:41
0

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

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

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

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

ответил Mark James 17 Jam1000000amSat, 17 Jan 2009 11:25:49 +030009 2009, 11:25:49
0

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

  

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

ответил Boojum 16 Jpm1000000pmFri, 16 Jan 2009 13:08:53 +030009 2009, 13:08:53
0

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

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

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

PS подумайте о других «простых в использовании» языках - все их методы виртуальны под прикрытием и в настоящее время не сканируются.

ответил gbjbaanb 16 Jam1000000amFri, 16 Jan 2009 11:28:43 +030009 2009, 11:28:43
0

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

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

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

Кстати, вот сообщение, которое я нашел , в котором говорится об аспектах производительности ЦП.

ответил Jason S 16 Jpm1000000pmFri, 16 Jan 2009 17:01:12 +030009 2009, 17:01:12
0

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

ответил Serge 16 Jam1000000amFri, 16 Jan 2009 11:31:53 +030009 2009, 11:31:53
0

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

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

ответил Daemin 16 Jam1000000amFri, 16 Jan 2009 11:58:46 +030009 2009, 11:58:46
0

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

Это хорошо видно из теста, разница во времени ~ 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;
}

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

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

ответил Evgueny Sedov 6 FebruaryEurope/MoscowbMon, 06 Feb 2012 23:10:45 +0400000000pmMon, 06 Feb 2012 23:10:45 +040012 2012, 23:10:45
0

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

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

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

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

ответил It'sPete 31 J000000Wednesday13 2013, 03:01:59
0

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

ответил Hurkyl 11 J000000Saturday15 2015, 00:58:08
0

Стоит отметить, что это:

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;
}

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

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

ответил nikdeapen 16 WedEurope/Moscow2015-12-16T03:22:16+03:00Europe/Moscow12bEurope/MoscowWed, 16 Dec 2015 03:22:16 +0300 2015, 03:22:16
0

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

ответил 16 Jam1000000amFri, 16 Jan 2009 11:41:05 +030009 2009, 11:41:05
0

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

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

// 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;
}

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

ответил christianparpart 29 PMpFri, 29 Apr 2011 22:31:07 +040031Friday 2011, 22:31:07

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132