Виртуальные функции и производительность - C ++ - PullRequest
112 голосов
/ 16 января 2009

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

Ответы [ 15 ]

162 голосов
/ 17 января 2009

Ваш вопрос вызвал у меня любопытство, поэтому я выбрал тактирование для процессора 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 нс на звонок)
  • прямой: 68 мс (5,53 нс на звонок)
  • виртуальный: 160 мс (13 нс на звонок)

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

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

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

86 голосов
/ 16 января 2009

Хорошее правило:

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

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

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

42 голосов
/ 16 января 2009

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

33 голосов
/ 17 января 2009

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

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

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

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

27 голосов
/ 16 января 2009

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

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

7 голосов
/ 16 января 2009

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

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

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

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

6 голосов
/ 16 января 2009

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

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

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

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

4 голосов
/ 16 января 2009

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

3 голосов
/ 06 февраля 2012

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

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

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

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

3 голосов
/ 16 января 2009

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

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

...