Потеря производительности при работе с интерфейсами в C ++? - PullRequest
42 голосов
/ 22 сентября 2008

Есть ли потеря производительности во время выполнения при использовании интерфейсов (абстрактных базовых классов) в C ++?

Ответы [ 16 ]

45 голосов
/ 22 сентября 2008

Краткий ответ: Нет.

Длинный ответ: Это не базовый класс или количество предков, которые класс имеет в своей иерархии, которые влияют на его скорость. Единственное, это стоимость вызова метода.

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

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

23 голосов
/ 22 сентября 2008

Функции, вызываемые с помощью виртуальной диспетчеризации, не являются встроенными

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

Стоимость виртуального звонка зависит от платформы

Что касается штрафа за накладные расходы по сравнению с обычным вызовом функции, ответ зависит от вашей целевой платформы. Если вы нацелены на ПК с процессором x86 / x64, штраф за вызов виртуальной функции очень мал, поскольку современный процессор x86 / x64 может выполнять прогнозирование ветвления при косвенных вызовах. Однако если вы нацелены на PowerPC или какую-либо другую платформу RISC, штраф за виртуальные вызовы может быть весьма значительным, поскольку на некоторых платформах никогда не прогнозируются косвенные вызовы (см. Рекомендации по кроссплатформенной разработке для ПК / Xbox 360 ).

9 голосов
/ 22 сентября 2008

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

5 голосов
/ 22 сентября 2008

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

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

4 голосов
/ 22 сентября 2008

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

class AbstractAlgo
{
    virtual int func();
};

class Algo1 : public AbstractAlgo
{
    virtual int func();
};

class Algo2 : public AbstractAlgo
{
    virtual int func();
};

void compute(AbstractAlgo* algo)
{
      // Use algo many times, paying virtual function cost each time

}   

int main()
{
    int which;
     AbstractAlgo* algo;

    // read which from config file
    if (which == 1)
       algo = new Algo1();
    else
       algo = new Algo2();
    compute(algo);
}

То же самое с использованием полиморфизма времени компиляции

class Algo1
{
    int func();
};

class Algo2
{
    int func();
};


template<class ALGO>  void compute()
{
    ALGO algo;
      // Use algo many times.  No virtual function cost, and func() may be inlined.
}   

int main()
{
    int which;
    // read which from config file
    if (which == 1)
       compute<Algo1>();
    else
       compute<Algo2>();
}
3 голосов
/ 22 сентября 2008

Большинство людей отмечают штраф за время выполнения, и это правильно.

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

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

3 голосов
/ 22 сентября 2008

Я не думаю, что сравнение затрат происходит между вызовом виртуальной функции и прямым вызовом функции. Если вы думаете об использовании абстрактного базового класса (интерфейса), то возникает ситуация, когда вы хотите выполнить одно из нескольких действий, основанных на динамическом типе объекта. Вы должны сделать этот выбор как-то. Одним из вариантов является использование виртуальных функций. Другой - переключение на тип объекта, либо через RTTI (потенциально дорогой), либо путем добавления метода type () к базовому классу (потенциально увеличивая использование памяти каждым объектом). Таким образом, стоимость вызова виртуальной функции должна сравниваться со стоимостью альтернативы, а не со стоимостью бездействия.

2 голосов
/ 12 октября 2008

Обратите внимание, что множественное наследование увеличивает размер объекта с помощью нескольких указателей vtable. С G ++ на x86, если у вашего класса есть виртуальный метод и нет базового класса, у вас есть один указатель на vtable. Если у вас есть один базовый класс с виртуальными методами, у вас все еще есть один указатель на vtable. Если у вас есть два базовых класса с виртуальными методами, у вас есть два vtable указателя на каждый экземпляр .

Таким образом, с множественным наследованием (что и есть реализация интерфейсов в C ++), вы платите базовым классам размер указателя в размере экземпляра объекта. Увеличение объема памяти может иметь косвенные последствия для производительности.

2 голосов
/ 22 сентября 2008

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

0 голосов
/ 05 ноября 2008

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

...