Оператор if против указателя на функцию

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

// if-statement

void action() { /* ... */ }


void someLoop() {

  if (checkboxTrue) {
    action();
  }
  // ... other stuff

}

Был бы код более производительным и чистым или каким-то другим образом лучше, если бы использовался указатель на функцию? Как это:

// function pointer

void action() { /* ... */ }
void empty() {}
void (*actionPtr)();


void checkboxChanged(int val) {

  if (val == 1)
    actionPtr = &realAction;
  else
    actionPtr = ∅

}

void someLoop() {

  (*actionPtr)();
  // ... other stuff

}

person Davorin    schedule 03.01.2014    source источник
comment
Нет. Простота — это хорошо. Всегда выбирайте удобочитаемость вместо производительности, если нет проблем с производительностью.   -  person Bart Friederichs    schedule 03.01.2014
comment
Я бы сделал это первым способом. Это короче и говорит, что это значит. Производительность не является проблемой для чего-то столь несущественного.   -  person Mike Dunlavey    schedule 03.01.2014
comment
Вы можете добиться обратного эффекта. Ветви можно прогнозировать, а вызовы функций встраивать. Однако вызов через указатель функции всегда влечет за собой косвенные затраты.   -  person Kerrek SB    schedule 03.01.2014
comment
@KerrekSB, это действительно хороший момент, можете ли вы опубликовать ответ?   -  person Davorin    schedule 03.01.2014
comment
Помимо очевидной ошибки в потенциальном вызове неопределенного указателя на функцию, я не вижу особого смысла во втором методе вообще. И если проверка состояния флажка действительно является недостатком производительности в доспехах вашего приложения, я очень стараюсь представить, насколько это просто. Несмотря ни на что, профиль, профиль, профиль...   -  person WhozCraig    schedule 03.01.2014
comment
Я не согласен со всеми здесь, я думаю, что указатель функции будет быстрее в целом. Хотя разница очень незначительна.   -  person old_timer    schedule 03.01.2014
comment
Если вам важна производительность, вы не будете использовать ни один из этих вариантов. Запуск цикла занятости для ожидания пользовательского ввода — самое плохое решение, которое только возможно.   -  person David Heffernan    schedule 03.01.2014
comment
Мой фактический вариант использования — цикл отображения 60 Гц * 10 визуальных элементов = 600 проверок в секунду.   -  person Davorin    schedule 03.01.2014
comment
KerrekSB: непрямые ответвления также можно предсказать, но, вероятно, менее точно, чем ответвления.   -  person dbrank0    schedule 03.01.2014


Ответы (6)


  1. Один косвенный вызов функции дороже, чем одно условие if.

  2. Несколько условий if обходятся дороже, чем косвенный вызов функции.

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

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

Однако, если вы обнаружите, что действительно используете сложные операторы if/else и/или switch внутри своего внутреннего цикла, вы можете оптимизировать их, заменив их косвенными вызовами функций.


Изменить:
Вы говорите, что у вас 600 проверок в секунду. Предполагая, что вам нужно обработать только один случай if (ситуация, когда if работает быстрее), вы «экономите» примерно 6 микросекунд в секунду, не используя косвенный указатель на функцию, что составляет 0,0006% времени выполнения. Усилий точно не стоит...

person cmaster - reinstate monica    schedule 03.01.2014
comment
Несколько условий if могут быть быстрее, чем один непрямой вызов. На последних процессорах Intel один косвенный вызов с точки зрения производительности эквивалентен примерно 3–4 ветвям (if). - person ; 03.01.2014
comment
@VladLazarenko Только если они правильно предсказаны. Вызов функции занимает от 10 до 20 циклов на самом деле, один доступ к кэшированной памяти - это задержка в четыре цикла, это от 14 до 24 циклов для косвенного вызова, игнорируя возможность предварительной загрузки указателя функции, которая снова уменьшит эту цифру. Два сброса конвейера из-за неправильного предсказания ветвления должны быть как минимум такими же затратными. - person cmaster - reinstate monica; 03.01.2014

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

extern void fun0 ( unsigned int );
extern void fun1 ( unsigned int );

void (*fun(unsigned int));


void dofun0 ( unsigned int x, unsigned int y )
{
    if(x) fun0(y);
    else  fun1(y);
}

void dofun ( unsigned int y )
{
    fun(y);
}

дает что-то вроде этого, например

Disassembly of section .text:

00000000 <dofun0>:
   0:   e3500000    cmp r0, #0
   4:   e92d4008    push    {r3, lr}
   8:   e1a00001    mov r0, r1
   c:   1a000002    bne 1c <dofun0+0x1c>
  10:   ebfffffe    bl  0 <fun1>
  14:   e8bd4008    pop {r3, lr}
  18:   e12fff1e    bx  lr
  1c:   ebfffffe    bl  0 <fun0>
  20:   e8bd4008    pop {r3, lr}
  24:   e12fff1e    bx  lr

00000028 <dofun>:
  28:   e92d4008    push    {r3, lr}
  2c:   ebfffffe    bl  0 <fun>
  30:   e8bd4008    pop {r3, lr}
  34:   e12fff1e    bx  lr

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

person old_timer    schedule 03.01.2014
comment
Истинный. Но если fun0 и fun1 видны компилятору, он может выбрать их встраивание, что снова меняет историю. - person dbrank0; 03.01.2014
comment
Вы абсолютно правы, и в зависимости от набора инструкций и других факторов может произойти все что угодно. Я бы предположил, что указатели функций были изобретены специально, чтобы избежать переключения или дерева if-then-else (по разным причинам), но это уже другая тема. Таким образом, либо вы можете получить что-то равное, либо если-то-иначе медленнее, хотя и немного. Это было очень легко продемонстрировать, более медленный случай с тщательно продуманным кодом. Точно так же легко продемонстрировать отсутствие разницы в коде, но я оставлю это на усмотрение читателя. - person old_timer; 03.01.2014
comment
Вероятно, можно придумать крайний случай или несколько, когда if-then-else производит более быстрый код. - person old_timer; 03.01.2014

Имхо, указатели немного чище для вызова подпрограмм с большим количеством аргументов:

int good(int a, int b, int c, char *d) { ... }
int bad (int a, int b, int c, char *d) { ... }
int ugly(int a, int b, int c, char *d) { ... }

Прямые звонки:

if (is_good) {
   good(1,2,3,"fire!");
} else if (is_bad) {
   bad(1,2,3,"fire!");
} else {
   ugly(1,2,3,"fire!");
}

Косвенные звонки:

if (is_good) {
   f = good;
} else if (is_bad) {
   f = bad;
} else {
   f = ugly;
}

...

f(1,2,3,"fire!");
person user2743554    schedule 03.01.2014

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

person deviantfan    schedule 03.01.2014

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

Возможно, другой вариант - поместить указатели функций проверки/действия в массив. И проверьте это внутри цикла. У вас есть чистая функциональность someLoop, и ваша проверка/действие ("бизнес-логика") находится внутри этого массива.

person duedl0r    schedule 03.01.2014

Если вы собираетесь проверять эти вещи только в одном центральном месте, я рекомендую использовать switch, если возможно, и if/else, если нет.

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

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

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

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

person Community    schedule 21.12.2017