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


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

15 106

15 ответов:

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

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

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

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

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

  • inline: 8 мс (0,65 НС на вызов)
  • сразу: 68мс (5.53 НС в звонок)
  • виртуальный: 160ms (13ns за вызов)

Итак, в этом случае (где все помещается в кэш) вызовы виртуальных функций были примерно в 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 тактов. Правила предсказания и неправильного предсказания виртуального вызовы функций-это то же самое, что и для операторов switch...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

вот какой датированный документ, который анализирует лучшие практики для 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. Однако время, необходимое для выполнения вызова, обычно очень мало по сравнению с временем, затраченным на фактическое выполнение вашей функции, поэтому общая стоимость производительности будет незначительной, особенно с учетом текущая производительность оборудования. Кроме того, компилятор иногда может оптимизировать и видеть, что никакой виртуальный вызов не требуется, и скомпилировать его в статический вызов. Так что не волнуйтесь, используйте виртуальные функции и абстрактные классы столько, сколько вам нужно.

Я всегда задавал себе этот вопрос, тем более, что - довольно много лет назад - я также сделал такой тест, сравнивая тайминги стандартного вызова метода-члена с виртуальным и был очень зол на результаты в то время, когда пустые виртуальные вызовы были в 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;
}

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