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

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

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

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

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

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

Ваш вопрос вызвал у меня любопытство, поэтому я пошел вперед и провел несколько таймингов на процессоре PowerPC с частотой 3 ГГц, с которым мы работаем. Тест, который я выполнял, состоял в том, чтобы создать простой векторный class 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) и запускал цикл, который добавлял их друг к другу (Ax = Bx + Cx) 1000 раз. Я запускал это с функциями, определенными как inline , virtual и обычные вызовы функций. Вот результаты:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 // g++ -std=c++0x -o perf perf.cpp -lrt #include  // typeid #include  // printf #include  // atoll #include  // 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 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 void test(unsigned long long count) { test(count); test(count); } int main(int argc, const char* argv[]) { test(argc == 2 ? atoll(argv[1]) : 10000000000llu); return 0; } 

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

  • Почему memcmp намного быстрее, чем проверка цикла?
  • Эффективное умножение векторных матриц 4x4 на SSE: горизонтальное добавление и точечный продукт - в чем смысл?
  • Является ли std :: vector намного медленнее, чем простые массивы?
  • Являются ли статические вызовы Java более или менее дорогостоящими, чем нестатические вызовы?
  • Анализ кода для эффективности?
  • Инструменты профилирования Delphi
  • Почему условный ход не уязвим для отказа от ветвления?
  • Распределение памяти / освобождение Узкое место?
  • Является ли рекурсивный-итеративный метод лучше, чем чисто итеративный метод, чтобы выяснить, является ли число простым?
  • Разница между rdtscp, rdtsc: памятью и cpuid / rdtsc?
  • Отладка и производительность релиза
  • Давайте будем гением компьютера.