Какова стоимость использования указателя на функцию-член по сравнению с использованием указателя на функцию-член?переключатель?

StackOverflow https://stackoverflow.com/questions/113150

  •  02-07-2019
  •  | 
  •  

Вопрос

У меня следующая ситуация:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

В моей текущей реализации я сохраняю значение whichFoo в элементе данных в конструкторе и используйте switch в callFoo() чтобы решить, какой из foo вызывать.В качестве альтернативы я могу использовать switch в конструкторе, чтобы сохранить указатель справа fooN() быть вызванным callFoo().

Мой вопрос в том, какой способ более эффективен, если объект класса A создается только один раз, а callFoo() вызывается очень большое количество раз.Таким образом, в первом случае у нас есть несколько исполнений оператора переключателя, а во втором — только один переключатель и несколько вызовов функции-члена с использованием указателя на него.Я знаю, что вызов функции-члена с использованием указателя медленнее, чем простой вызов ее напрямую.Кто-нибудь знает, превышают ли эти накладные расходы стоимость switch?

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

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

Это было полезно?

Решение

Насколько вы уверены, что вызов функции-члена через указатель медленнее, чем простой вызов?Можете ли вы измерить разницу?

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

Больше информации:Есть отличная статья Указатели на функции-члены и самые быстрые делегаты C++ который очень подробно описывает реализацию указателей на функции-члены.

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

Вы можете написать это:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

Вычисление фактического указателя функции происходит линейно и быстро:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

Обратите внимание, что вам даже не нужно фиксировать фактический номер функции в конструкторе.

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

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

Чтобы ответить на незаданный вопрос:что здесь значит «лучше»?В большинстве случаев я ожидаю, что разница будет незначительной.Однако в зависимости от того, какой класс он делает, разница может быть значительной.Тестирование производительности, прежде чем беспокоиться о разнице, очевидно, является правильным первым шагом.

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

Примечание:Я не работал с C++ со времен колледжа, поэтому мой жаргон может быть неправильным, но общие идеи справедливы для большинства объектно-ориентированных языков.

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

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

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

Похоже, вам следует сделать callFoo чистую виртуальную функцию и создайте несколько подклассов A.

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

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

Я должен думать, что указатель будет быстрее.

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

Конечно, вам следует измерить и то, и другое.

Оптимизируйте только при необходимости

Первый:В большинстве случаев вам, скорее всего, все равно, разница будет очень небольшой.Сначала убедитесь, что оптимизация этого вызова действительно имеет смысл.Только если ваши измерения покажут действительно значительные затраты времени на разговор, приступайте к его оптимизации (бессовестная вилка – Ср. Как оптимизировать приложение, чтобы оно работало быстрее?) Если оптимизация незначительна, отдайте предпочтение более читаемому коду.

Стоимость непрямого звонка зависит от целевой платформы

Как только вы определили, что стоит применить низкоуровневую оптимизацию, пришло время понять вашу целевую платформу.Цена, которую здесь можно избежать, — это штраф за неправильное предсказание ветвей.На современных процессорах x86/x64 это неверное предсказание, вероятно, будет очень небольшим (в большинстве случаев они могут довольно хорошо предсказывать непрямые вызовы), но при работе с PowerPC или другими RISC-платформами косвенные вызовы/переходы часто вообще не прогнозируются и избегаются. они могут привести к значительному увеличению производительности.Смотрите также Стоимость виртуального звонка зависит от платформы.

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

Одна ошибка:Иногда переключатель можно реализовать и как косвенный вызов (с использованием таблицы), особенно при переключении между многими возможными значениями.Такой переключатель демонстрирует те же ошибки прогнозирования, что и виртуальная функция.Чтобы сделать эту оптимизацию надежной, в наиболее распространенном случае, вероятно, предпочтительнее использовать if вместо switch.

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

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

Этот метод широко используется в ОС Symbian:http://www.titu.jyu.fi/modpa/Patterns/pattern-TwoPhaseConstruction.html

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

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

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

пс.Чтобы подкрепить ответ Грега: если вас волнует скорость — измеряйте.Просмотр ассемблера не помогает, когда процессоры имеют упреждающую выборку/предсказанное ветвление, остановки конвейера и т. д.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top