Подтвердить что ты не робот

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

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


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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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, сгенерированным с помощью switch. Версия switch не обеспечивает увеличения производительности.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

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

Конечно, вы должны измерить оба.

Ответ 9

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

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

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

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

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

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

Ответ 10

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

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

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

Ответ 11

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

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

Ответ 12

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

пс. Чтобы усилить ответ greg, если вам нужна скорость. Глядя на ассемблер, не помогает, когда у процессоров есть префабрикаты/прогностические ветвления и конвейеры и т.д.