Штраф за производительность при работе с интерфейсами на C++?

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

Ответы на вопрос это также связаны.

Richard Corden 22.09.2008 13:28
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
46
1
14 247
16
Перейти к ответу Данный вопрос помечен как решенный

Ответы 16

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

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

Единственное главное отличие, о котором я знаю, заключается в том, что, поскольку вы не используете конкретный класс, встраивание (намного?) Сложнее.

Ответ принят как подходящий

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

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

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

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

Ничего не стоит, что вызов виртуального метода для объекта с несколькими базовыми классами имеет немного более высокую стоимость, чем вызов виртуального метода для объекта с единственной иерархией наследования.

Greg Hewgill 22.09.2008 12:47

Уверены ли вы? У вас есть источник этого комментария, который я могу проверить?

Martin York 22.09.2008 12:52

Стоимость указана в пересчете. Если у вас есть 'D *' и вы хотите преобразовать в 'B2 *', тогда, когда макет класса - [B1, B2], компилятор должен вернуть 'D * + смещение в B2'. Я не согласен с тем, что это стоит отметить, но это будет несущественно.

Richard Corden 22.09.2008 13:25

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

Steve Jessop 30.09.2008 20:20

@Martin: На самом деле виртуальный метод будет встроен именно в тех случаях, когда был бы не виртуальный метод, а именно, когда динамический тип объекта может быть определен во время компиляции, а метод является встроенным (не слишком большим + объявленным либо в определении класса, либо с "встроенным").

j_random_hacker 02.04.2009 09:21

Было бы интересно более подробно объяснить, что такое случается с виртуальным вызовом ... то есть в коде C / ASM, что на самом деле содержит vtable средства

Mr. Boy 24.10.2011 14:44

@ Джон: Это старый вопрос. Если вы хотите знать, задайте новый вопрос. Хотя вы, вероятно, получите удар по голове, сказав, что это не актуально, поскольку vtables - это деталь реализации и не требуется стандартом.

Martin York 24.10.2011 21:18

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

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

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

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

Не позволяйте этому испортить ваш дизайн, используйте виртуальные функции только в том случае, если они вам действительно нужны - встраивание может привести к ОГРОМНОМУ приросту производительности, когда вы думаете об итерации по (большому) количеству элементов и вызову метода для каждого из них их.

xtofl 22.09.2008 12:53

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

j_random_hacker 16.01.2009 22:04

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

Да, но, насколько мне известно, ничего примечательного. Падение производительности происходит из-за «косвенности» при каждом вызове метода.

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

Если вы хотите быть уверенным, вам следует запустить свои собственные тесты.

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

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

André Caron 18.10.2010 22:46

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

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

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

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

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

j_random_hacker 16.01.2009 22:03

@j_random_hacker - это только тот случай, когда вы обычно используете его как указатель на базу.

Ghita 30.06.2012 10:20

@Ghita: Даже тогда оптимизирующий компилятор может определить, каким должен быть динамический тип объекта. Я предполагаю, что большинство компиляторов могут встроить вызов foo() в Base* x = new Derived; x->foo();.

j_random_hacker 30.06.2012 13:23

@j_random_hacker да, может. Но в нормальном случае использования вы обычно передаете указатель на базовый класс. Однако интересно отметить, что простейшие случаи обрабатываются компилятором.

Ghita 02.07.2012 21:37

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

Lothar 12.01.2019 08:07

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

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

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

К сожалению, это не относится к классам плагинов и другим динамически загружаемым типам (да, это является возможно в C++ :-)

André Caron 18.10.2010 22:49

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

Lothar 12.01.2019 08:10

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

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

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

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

Я хотел бы предложить контраргумент: для встраиваемых систем (другие упоминали игровые приставки) снижение производительности может быть слишком большим, и если не будет получено много пользы от ремонтопригодности / удобочитаемости / ясности и т. д., Их следует избегать.

It'sPete 31.07.2013 02:07

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

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

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

Другие вопросы по теме