Указатель на функцию замедляет работу программы? - PullRequest
51 голосов
/ 13 марта 2010

Я читал о функциональных указателях на C. И все говорили, что это заставит мою программу работать медленно. Это правда?

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

Так что, плохо использовать указатель на функцию? Заранее спасибо.

В ответ на некоторых парней. Я сказал «беги медленно» за то время, которое я сравнил в цикле. как это:

int end = 1000;
int i = 0;

while (i < end) {
 fp = func;
 fp ();
}

Когда вы выполните это, у меня будет то же время, если я выполню это.

while (i < end) {
 func ();
}

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

Ответы [ 8 ]

81 голосов
/ 13 марта 2010

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

Это может звучать странно для людей, которые привыкли думать о коде C как о чем-то, выполняемом абстрактной машиной C, чей «машинный язык» близко отражает сам язык C. В таком контексте «по умолчанию» косвенный вызов функции действительно медленнее, чем прямой, потому что формально он предусматривает дополнительный доступ к памяти для определения цели вызова.

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

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

// Direct call
do-it-many-times
  call 0x12345678

// Indirect call
do-it-many-times
  call dword ptr [0x67890ABC]

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

Теперь давайте вспомним, что архитектура x86 на самом деле имеет еще один способ предоставления операнда для инструкции call. Он предоставляет целевой адрес в регистре . И очень важным моментом в этом формате является то, что он обычно быстрее, чем оба указанных выше . Что это значит для нас? Это означает, что хороший оптимизирующий компилятор должен и воспользуется этим фактом. Для реализации вышеуказанного цикла компилятор попытается использовать вызов через регистр в обоих случаях. В случае успеха окончательный код может выглядеть следующим образом

// Direct call

mov eax, 0x12345678

do-it-many-times
  call eax

// Indirect call

mov eax, dword ptr [0x67890ABC]

do-it-many-times
  call eax

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

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

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

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

25 голосов
/ 13 марта 2010

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

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

9 голосов
/ 13 марта 2010

И все говорили, что это заставит мою программу работать медленно.Это правда?

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

if (condition1) {
        func1();
} else if (condition2)
        func2();
} else if (condition3)
        func3();
} else {
        func4();
}

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

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

9 голосов
/ 13 марта 2010

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

Самое распространенное место, где вы найдете указатели функций в API c / c ++, это функции обратного вызова. Причина, по которой так много API делают это, заключается в том, что написание системы, которая вызывает указатель на функцию всякий раз, когда происходят события, гораздо более эффективно, чем другие методы, такие как передача сообщений. Лично я также использовал указатели функций как часть более сложной системы обработки ввода, где каждая клавиша на клавиатуре имеет указатель функции, сопоставленный с ней через таблицу переходов. Это позволило мне удалить любую ветвление или логику из системы ввода и просто обработать нажатие входящей клавиши.

8 голосов
/ 13 марта 2010

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

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

7 голосов
/ 13 марта 2010

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

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

6 голосов
/ 17 марта 2010

Много хороших моментов в предыдущих ответах.

Однако взгляните на функцию сравнения C qsort. Поскольку функция сравнения не может быть встроенной и должна следовать стандартным соглашениям о вызовах на основе стека, общее время выполнения для сортировки может быть на порядка (точнее 3-10x) медленнее для целочисленных ключей, чем в противном случае тот же код с прямым, встроенным вызовом.

Типичным встроенным сравнением будет последовательность простых CMP и, возможно, CMOV / SET. Вызов функции также влечет за собой издержки CALL, настройку фрейма стека, выполнение сравнения, разрушение фрейма стека и возврат результата. Обратите внимание, что операции стека могут вызывать задержки конвейера из-за длины конвейера ЦП и виртуальных регистров. Например, если значение say eax необходимо до того, как инструкция, по которой последний измененный eax завершил выполнение (обычно это занимает около 12 тактов на новейших процессорах). Если процессор не сможет выполнить другие инструкции, чтобы дождаться этого, произойдет остановка конвейера.

0 голосов
/ 28 февраля 2018

Возможно.

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

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

void foo(int* x) {
    *x = 0;
}

void (*foo_ptr)(int*) = foo;

int call_foo(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo(&r);
    return r;
}

int call_foo_ptr(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo_ptr(&r);
    return r;
}

Сгенерированный код для call_foo():

call_foo(int*, int):
  xor eax, eax
  ret

Ницца.foo() был не только встроен, но и позволил компилятору исключить весь предыдущий цикл!Сгенерированный код просто обнуляет регистр возврата с помощью XOR с самим регистром, а затем возвращает.С другой стороны, компиляторы должны будут генерировать код для цикла в call_foo_ptr() (100+ строк с gcc 7.3), и большая часть этого кода фактически ничего не делает (пока foo_ptr все еще указывает на foo()).(В более типичных сценариях можно ожидать, что вставка небольшой функции в горячий внутренний цикл может сократить время выполнения примерно на порядок.)

Так что в худшем случае вызов указателя функциипроизвольно медленнее, чем прямой вызов функции, но это вводит в заблуждение.Оказывается, что если бы foo_ptr было const, то call_foo() и call_foo_ptr() сгенерировали бы один и тот же код.Однако это потребует от нас отказаться от возможности косвенного обращения, предоставленной foo_ptr.«Справедливо» ли для foo_ptr быть const?Если нас интересует косвенность, предоставляемая foo_ptr, то нет, но если это так, то прямой вызов функции также недопустим.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...